Explicação detalhada do SpringBoot Integrated WebSocket

Graças aos blogueiros que se referiram ao artigo, a visão geral e o uso do WebSocket estão escritos em detalhes. Aqui, eu o organizei com base no meu próprio entendimento.

1. Visão geral do WebSocket

1. Introdução ao WebSocket

O protocolo WebSocket é um novo protocolo de rede baseado em TCP. Ele implementa a comunicação full-duplex (full-duplex) entre o navegador e o servidor - permitindo que o servidor envie informações ativamente para o cliente.

A figura é do artigo de referência:

insira a descrição da imagem aqui

2. Por que você precisa do WebSocket?

O HTTP é baseado em solicitação-resposta, ou seja, a comunicação só pode ser iniciada pelo cliente, e o servidor responde, sem estado e sem conexão.

  • Stateless: apenas uma solicitação é processada por conexão e a conexão é desconectada após o término da solicitação.
  • Sem conexão: não há memória para o processamento da transação e o servidor não sabe em que estado o cliente está.

Para realizar mensagens instantâneas por meio de HTTP, apenas a pesquisa de página envia uma solicitação ao servidor e o servidor retorna o resultado da consulta. O polling é ineficiente e desperdiça recursos porque deve continuar se conectando ou a conexão HTTP está sempre aberta.

A maior característica do WebSocket é que o servidor pode enviar informações ativamente para o cliente, e o cliente também pode enviar informações ativamente para o servidor, o que é um verdadeiro diálogo igualitário bidirecional.

Recursos do WebSocket:

  • (1) Com base no protocolo TCP, a implementação do lado do servidor é relativamente fácil.
  • (2) Possui boa compatibilidade com o protocolo HTTP. As portas padrão também são 80 e 443, e a fase de handshake usa o protocolo HTTP, portanto, não é fácil proteger durante o handshake e pode passar por vários servidores proxy HTTP.
  • (3) O formato de dados é relativamente leve, a sobrecarga de desempenho é pequena e a comunicação é eficiente.
  • (4) Texto ou dados binários podem ser enviados.
  • (5) Não há restrição de mesma origem e o cliente pode se comunicar com qualquer servidor.
  • (6) O identificador de protocolo é ws (ou wss se criptografado) e a URL do servidor é a URL.

A figura é do artigo de referência:

insira a descrição da imagem aqui

2. SpringBoot integra WebSocket

Crie um projeto SpringBoot e introduza as dependências do WebSocket. O front-end é relativamente simples aqui.

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

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

application.yml:

server:
  port: 8081

spring:
  thymeleaf:
    mode: HTML
    cache: true
    prefix: classpath:/templates/
    encoding: UTF-8
    suffix: .html
    check-template-location: true
    template-resolver-order: 1

1, WebSocketConfig

Ativar o suporte WebSocket também é muito simples.

/**
 * WebSocket配置类。开启WebSocket的支持
 */
@Configuration
public class WebSocketConfig {
    
    

    /**
     * bean注册:会自动扫描带有@ServerEndpoint注解声明的Websocket Endpoint(端点),注册成为Websocket bean。
     * 要注意,如果项目使用外置的servlet容器,而不是直接使用springboot内置容器的话,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    
    
        return new ServerEndpointExporter();
    }

}

2, WebSocketServer

Aqui está o ponto chave, o núcleo está aqui.

  • Como o WebSocket é semelhante à forma de servidor cliente ( 采用ws协议), o WebSocketServer aqui é realmente equivalente a um controlador do protocolo ws
  • 直接@ServerEndpoint("/imserver/{userId}") 、@Component启用即可e implemente @OnOpen para abrir a conexão, @onClose para fechar a conexão, @onMessage para receber mensagens e outros métodos.
  • Crie um novo ConcurrentHashMap用于接收当前userId的WebSocket或者Session信息para facilitar mensagens push para userId entre mensagens instantâneas. A versão autônoma pode ser realizada aqui.
  • A versão do cluster (vários nós ws) também precisa usar MySQL ou Redis para executar o processamento de transmissão de assinatura e só precisa modificar o método sendMessage correspondente.
/**
 * WebSocket的操作类
 */
@Component
@Slf4j
/**
 * html页面与之关联的接口
 * var reqUrl = "http://localhost:8081/websocket/" + cid;
 * socket = new WebSocket(reqUrl.replace("http", "ws"));
 */
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {
    
    

    /**
     * 静态变量,用来记录当前在线连接数,线程安全的类。
     */
    private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0);

    /**
     * 存放所有在线的客户端
     */
    private static Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();

    /**
     * 连接sid和连接会话
     */
    private String sid;
    private Session session;

    /**
     * 连接建立成功调用的方法。由前端<code>new WebSocket</code>触发
     *
     * @param sid     每次页面建立连接时传入到服务端的id,比如用户id等。可以自定义。
     * @param session 与某个客户端的连接会话,需要通过它来给客户端发送消息
     */
    @OnOpen
    public void onOpen(@PathParam("sid") String sid, Session session) {
    
    
        /**
         * session.getId():当前session会话会自动生成一个id,从0开始累加的。
         */
        log.info("连接建立中 ==> session_id = {}, sid = {}", session.getId(), sid);
        //加入 Map中。将页面的sid和session绑定或者session.getId()与session
        //onlineSessionIdClientMap.put(session.getId(), session);
        onlineSessionClientMap.put(sid, session);

        //在线数加1
        onlineSessionClientCount.incrementAndGet();
        this.sid = sid;
        this.session = session;
        sendToOne(sid, "连接成功");
        log.info("连接建立成功,当前在线数为:{} ==> 开始监听新连接:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
    }

    /**
     * 连接关闭调用的方法。由前端<code>socket.close()</code>触发
     *
     * @param sid
     * @param session
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid, Session session) {
    
    
        //onlineSessionIdClientMap.remove(session.getId());
        // 从 Map中移除
        onlineSessionClientMap.remove(sid);

        //在线数减1
        onlineSessionClientCount.decrementAndGet();
        log.info("连接关闭成功,当前在线数为:{} ==> 关闭该连接信息:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
    }

    /**
     * 收到客户端消息后调用的方法。由前端<code>socket.send</code>触发
     * * 当服务端执行toSession.getAsyncRemote().sendText(xxx)后,前端的socket.onmessage得到监听。
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
    
    
        /**
         * html界面传递来得数据格式,可以自定义.
         * {"sid":"user-1","message":"hello websocket"}
         */
        JSONObject jsonObject = JSON.parseObject(message);
        String toSid = jsonObject.getString("sid");
        String msg = jsonObject.getString("message");
        log.info("服务端收到客户端消息 ==> fromSid = {}, toSid = {}, message = {}", sid, toSid, message);

        /**
         * 模拟约定:如果未指定sid信息,则群发,否则就单独发送
         */
        if (toSid == null || toSid == "" || "".equalsIgnoreCase(toSid)) {
    
    
            sendToAll(msg);
        } else {
    
    
            sendToOne(toSid, msg);
        }
    }

    /**
     * 发生错误调用的方法
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
    
    
        log.error("WebSocket发生错误,错误信息为:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 群发消息
     *
     * @param message 消息
     */
    private void sendToAll(String message) {
    
    
        // 遍历在线map集合
        onlineSessionClientMap.forEach((onlineSid, toSession) -> {
    
    
            // 排除掉自己
            if (!sid.equalsIgnoreCase(onlineSid)) {
    
    
                log.info("服务端给客户端群发消息 ==> sid = {}, toSid = {}, message = {}", sid, onlineSid, message);
                toSession.getAsyncRemote().sendText(message);
            }
        });
    }

    /**
     * 指定发送消息
     *
     * @param toSid
     * @param message
     */
    private void sendToOne(String toSid, String message) {
    
    
        // 通过sid查询map中是否存在
        Session toSession = onlineSessionClientMap.get(toSid);
        if (toSession == null) {
    
    
            log.error("服务端给客户端发送消息 ==> toSid = {} 不存在, message = {}", toSid, message);
            return;
        }
        // 异步发送
        log.info("服务端给客户端发送消息 ==> toSid = {}, message = {}", toSid, message);
        toSession.getAsyncRemote().sendText(message);
        /*
        // 同步发送
        try {
            toSession.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("发送消息失败,WebSocket IO异常");
            e.printStackTrace();
        }*/
    }

}

3、controlador

Existe apenas uma operação de salto de interface simples no controlador, e as outras não são necessárias.

@Controller
@RequestMapping("/demo")
public class DemoController {
    
    

    /**
     * 跳转到websocketDemo.html页面,携带自定义的cid信息。
     * http://localhost:8081/demo/toWebSocketDemo/user-1
     *
     * @param cid
     * @param model
     * @return
     */
    @GetMapping("/toWebSocketDemo/{cid}")
    public String toWebSocketDemo(@PathVariable String cid, Model model) {
    
    
        model.addAttribute("cid", cid);
        return "websocketDemo";
    }

}

4、websocketDemo.html

Crie um novo arquivo e coloque-o no diretório de modelos. A página simplesmente usa o código js para chamar o WebSocket.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>666666</title>
</head>
<body>
    传递来的数据值cid:
    <input type="text" th:value="${cid}" id="cid"/>
    <p>【toUserId】:
    <div><input id="toUserId" name="toUserId" type="text" value="user-1"></div>
    <p>【toUserId】:
    <div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
    <p>【操作】:
    <div>
        <button type="button" onclick="sendMessage()">发送消息</button>
    </div>
</body>

<script type="text/javascript">
    var socket;
    if (typeof (WebSocket) == "undefined") {
      
      
        console.log("您的浏览器不支持WebSocket");
    } else {
      
      
        console.log("您的浏览器支持WebSocket");
        //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接

        var cid = document.getElementById("cid").value;
        console.log("cid-->" + cid);
        var reqUrl = "http://localhost:8081/websocket/" + cid;
        socket = new WebSocket(reqUrl.replace("http", "ws"));
        //打开事件
        socket.onopen = function () {
      
      
            console.log("Socket 已打开");
            //socket.send("这是来自客户端的消息" + location.href + new Date());
        };
        //获得消息事件
        socket.onmessage = function (msg) {
      
      
            console.log("onmessage--" + msg.data);
            //发现消息进入    开始处理前端触发逻辑
        };
        //关闭事件
        socket.onclose = function () {
      
      
            console.log("Socket已关闭");
        };
        //发生了错误事件
        socket.onerror = function () {
      
      
            alert("Socket发生了错误");
            //此时可以尝试刷新页面
        }
        //离开页面时,关闭socket
        //jquery1.8中已经被废弃,3.0中已经移除
        // $(window).unload(function(){
      
      
        //     socket.close();
        //});
    }

    function sendMessage() {
      
      
        if (typeof (WebSocket) == "undefined") {
      
      
            console.log("您的浏览器不支持WebSocket");
        } else {
      
      
            // console.log("您的浏览器支持WebSocket");
            var toUserId = document.getElementById('toUserId').value;
            var contentText = document.getElementById('contentText').value;
            var msg = '{"sid":"' + toUserId + '","message":"' + contentText + '"}';
            console.log(msg);
            socket.send(msg);
        }
    }

</script>
</html>

5. Teste o efeito da operação

(1) Visite a página e estabeleça uma conexão

Inicie o projeto, visite http://localhost:8081/demo/toWebSocketDemo/{cid} para pular para a página e então você pode interagir com o WebSocket.

Aqui, abra três janelas do navegador:

http://localhost:8081/demo/toWebSocketDemo/user-1
http://localhost:8081/demo/toWebSocketDemo/user-2
http://localhost:8081/demo/toWebSocketDemo/user-3

Em seguida, abra o console do navegador. Neste momento, as informações de saída no console de ideias são as seguintes. Indica que a conexão foi estabelecida com sucesso.

insira a descrição da imagem aqui

insira a descrição da imagem aqui

(2) Especifique sid para enviar uma mensagem

user-2 envia dados para user-1 e também pode enviar dados para si mesmo.

A janela especificada pode receber dados, mas outras janelas não podem receber dados.

insira a descrição da imagem aqui

(3) Envio de mensagens em grupo

O grupo user-3 envia dados.

As condições para definir o envio em grupo no código são: quando toUserid não for especificado, trata-se de envio em grupo.

insira a descrição da imagem aqui

Você aprendeu.

Artigo de referência:

– Se você tem fome de conhecimento, seja humilde se for tolo.

おすすめ

転載: blog.csdn.net/qq_42402854/article/details/130948270