Spring WebSocket实现实时通讯

目录

一.什么是WebSocket?

二.HTTP与WebSocket的区别

三.Spring Websocket的使用步骤

1.添加依赖

2.通过配置类注册WebSocket端点

3.WebSocket测试类详解

1.连接成功时的注解@OnOpen

2.客户端发送消息时的注解@OnMessage

3.@OnClose和@OnError

4.参数接收@PathParam

5.Session对象的相关方法

四.HTML前端页面代码

五.多人在线聊天方法


一.什么是WebSocket?

Web的交互过程一般是通过客户端即浏览器向服务端发出一个请求,而服务端将请求接收之后将结果返回给客户端(至此双方的连接就算断开了),客户端将数据呈现在页面上。但是在生活中我们经常会遇见如新闻的实时推送和股票证券的信息实时推送还有体育足球的比赛信息的推送等,这些如果基于传统的请求相应模式即HTTP请求的话一般就是通过轮询的方式来获取信息。

        轮询:就是客户端每个一段时间就向服务端发起请求,以此保证信息的实时更新。当客户端以一定的高频率向服务端发起请求的时候,此时服务端数据还未更新不说且大大的增加了服务器的压力,浪费了大量的带宽。

此时一种不同于传统的通信方式WebSocket出现了,它恰恰能够在数据的实时更新上突出优点。

WebSocket是一种网络通信协议,它提供了一个全双工、双向的通信通道。通过 WebSocket,客户端和服务器之间可以进行实时、低延迟的双向通信。WebSocket 在客户端和服务器之间创建一个持久的连接,并允许双方在这个连接上随时交换数据,而不需要每次都重新建立连接,WebSocket 协议是在 HTTP 协议基础上进行扩展的,但它的工作方式与 HTTP 请求/响应模式不同,具有实时性和高效性的特点。

二.HTTP与WebSocket的区别

        1.通信模型

        HTTP 是基于请求/响应模式的协议。客户端向服务器发送请求,服务器处理请求并返回响应。每一次请求/响应都会建立新的连接,并在完成后关闭连接。
        WebSocket 是基于全双工的通信协议,一旦建立连接,客户端和服务器之间的通信是持久的,可以在连接中随时双向传输数据,直到连接被主动关闭。

        2.连接方式

        HTTP:每次请求都需要建立新的连接,传输完数据后关闭连接。
        WebSocket:通过一次握手(基于 HTTP 协议)建立连接后,连接会一直保持,直到一方主动关闭连接。

        3.数据传输方式

        HTTP:数据是按请求和响应的模式传输的,每次请求只能得到一个响应。
        WebSocket:连接保持开放,数据可以随时双向流动,无需再次发送请求,支持服务器主动向客户端推送数据。

        4.带宽和效率

        HTTP:每次请求/响应都会携带请求头、响应头等冗余信息,因此传输效率较低。
        WebSocket:建立连接后,数据传输无需携带过多的冗余信息,传输效率较高。

         5.协议类型

        HTTP:HTTP 协议是无状态的,客户端和服务器之间不会维持长期连接。
        WebSocket:WebSocket 是有状态的,客户端和服务器之间的连接是持久的,可以维持长期连接。

                                               WebSocket和HTTP在数据传输上的区别

三.Spring Websocket的使用步骤

1.添加依赖

首先我们要使用spring websocket就要先在当前我们spring项目里面引入对应依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
//Spring WebSocket的依赖

2.通过配置类注册WebSocket端点

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
@Slf4j
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        log.info("注册WebSocket端点");
        return new ServerEndpointExporter();
    }
}

这个配置类的作用是在项目启动的时候会调用有@Bean注解的方法,在这个类里面的方法作用是通过ServerEndpointExporter,它会扫描所有 @ServerEndpoint 注解的类匹配的端点,并将它们注册到 Servlet 容器中。所被注册到的端点才能被成功识别。

到这里可能会有人会有一些疑惑:为什么我们要用这个配置类?为什么不能直接通过依赖注入来将我们写的WebSocket操作的类(TestWebSocket 下面给出)直接交给spring容器管理?

原因:

当我们给TestWebSocket这个我们自己封装的类添加@ServerEndpoint注解的时候,它并不会如愿的交给spring容器管理,因为@ServerEndpoint标注的类不是 Spring Bean,无法直接注入,我们还要搭配则@Compnoent注解

WebSocket是相当于网络请求相关的类。你们应该也接触过HttpServletRequest和HttpServletResponse这个类,它也算是网络请求相关的类。而对于这些类,他们是交给服务器的容器管理的即Servlet容器,而Servlet容器是交给我们的服务器管理的,比如说Tomcat服务器。

交给Servlet容器管理的意义:在这个例子里面,我们将通过ServerEndpointExporter,它会扫描所有 @ServerEndpoint 注解的类,并将它们注册到 Servlet 容器中。而Servlet容器会能够识别并处理WebSocket协议的请求,我们在收到匹配 /ws 的 HTTP 请求且包含 Upgrade: websocket 头时,Servlet容器会将其升级为 WebSocket 连接。Servlet 容器负责创建和管理 WebSocket 端点类的实例,每个客户端连接会触发容器创建一个新的端点实例(默认行为),连接关闭后,容器可能销毁该实例(取决于实现)。

如果没有注册的后果:

如果未将端点注册到 Servlet 容器:
1.握手请求被忽略
2.客户端尝试连接 WebSocket 时,会收到 404 错误,因为 Servlet 容器未将路径映射到任何端点。
3.无法建立双向通信
4.后续的 WebSocket 消息无法被正确处理,连接无法建立。

上面就能够完全解释为什么我们将封装的WebSocket操作类不能直接通过@Component注解够交给spring容器管理而要先注册。

3.WebSocket测试类详解

1.连接成功时的注解@OnOpen

@Component
@ServerEndpoint("/message")//参数是发起请求的路径
public class TestWebsocket {
    //用于存放已连接的session会话
    private static Map<String, Session> map =new HashMap();

    @OnOpen//用于在连接成功的时候要进行的操作上添加的注解
    public void onOpen(Session session){
        //在客户端与服务端连接的时候,在OnOpen注解的方法中可接收一个Session参数,能够通过这个参数获取到当前这个会话的信息
        map.put(session.getId(),session);//session.getId()获取这个会话的id
        System.out.println(session.getId()+"连接成功");
    }

}

@OnOpen是添加在在连接成功时调用的方法上面,而这个session是与客户端连接成功的会话对象,通过这个会话对象能拿到此次会话的信息。在代码里面我们将这个会话通过HashMap保存起来的原因是因为我们要在后面通过map里面的会话来对客户端发消息

session.getid()的作用是返回当前连接成功的会话的id

我们观察控制台

2.客户端发送消息时的注解@OnMessage

当前端连接成功并发送消息来的时候,跨域搭配着@OnOpen将存储在map里面的所有的会话都进行应该群发效果

import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;


import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Component
@ServerEndpoint("/message")//参数是发起请求的路径
public class TestWebsocket {
    //用于存放已连接的session会话
    private static Map<String, Session> map =new HashMap();

    @OnOpen
    public void onOpen(Session session) {
        map.put(session.getId(), session);
    }
    @OnMessage//发送信息到服务端时的注解
    public void onMessage(String message, Session session) throws IOException {
            //String message用于接收发送来的字符串
        System.out.println(session.getId()+"说:"+message);
        Collection<Session> values = map.values();//获取到当前map所有的会话
        for (Session s :values){
            s.getBasicRemote().sendText(message);//将接收的信息群发给当前所有已经连接的会话
        }

    }

}

控制台效果:

浏览器接收的消息将其渲染在页面中的效果

3.@OnClose和@OnError

这里不再过多赘述,OnClose是在关闭连接时的调用的方法,OnError是在出错的时候调用的方法

import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;


import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Component
@ServerEndpoint("/message")//参数是发起请求的路径
public class TestWebsocket {
    //用于存放已连接的session会话
    private static Map<String, Session> map =new HashMap();

    @OnOpen
    public void onOpen(Session session) {
        map.put(session.getId(), session);
    }
    @OnMessage//发送信息到服务端时的注解
    public void onMessage(String message, Session session) throws IOException {
            //String message用于接收发送来的字符串
        System.out.println(session.getId()+"说:"+message);
        Collection<Session> values = map.values();//获取到当前map所有的会话
        for (Session s :values){
            s.getBasicRemote().sendText(message);//将接收的信息群发给当前所有已经连接的会话
        }

    }
    @OnClose//断开时要进行的方法上添加
    public void onClose(Session session) {
        map.remove(session.getId());
        System.out.println(session.getId()+"已断开连接");
    }
    @OnError//出现错误时调用的方法
    public void onError(Session session, Throwable throwable) {
        throwable.printStackTrace();
    }

}

4.参数接收@PathParam

在浏览器发来的请求可能携带路径参数,此时我们通过@PathParam注解来接收,括号里面填的是路径参数的名称

@ServerEndpoint("/message/{sid}")//配置发起连接的路径
public class WebSocketServer {

    //存放会话对象,通过操作这个map来新增session和删除session
    private static Map<String, Session> sessionMap = new HashMap();

    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

}

5.Session对象的相关方法

我发现session对象还是有点丰富的,所以这里列出其部分的方法使用

//返回当前WebSocket会话的唯一标识符。
String sessionId = session.getId();

//获取会话的属性,返回一个 Map<String, Object>,你可以在这里存储和检索用户信息等自定义数据。
Map<String, Object> attributes = session.getAttributes();
attributes.put("username", "user1");//客户端自定义数据

//发送消息到WebSocket客户端,WebSocketMessage 是发送的消息内容。
TextMessage message = new TextMessage("Hello, WebSocket!");
session.sendMessage(message);

//获取连接的远程地址(IP 地址)。这对检查连接来源或记录日志很有用。
InetSocketAddress remoteAddress = session.getRemoteAddress();

//检查WebSocket会话是否仍然处于打开状态。如果连接已经关闭或断开,返回 false。
boolean isOpen = session.isOpen();

//关闭WebSocket会话,并发送关闭状态。你可以传递一个 CloseStatus 对象来表示关闭的状态码。
session.close(CloseStatus.NORMAL);

//获取WebSocket握手的头信息,返回一个HttpHeaders对象。可以用于查看WebSocket握手时的HTTP头部信息。
HttpHeaders headers = session.getHandshakeHeaders();

//获取 WebSocket 会话的 URI 信息。
URI uri = session.getUri();

四.HTML前端页面代码

可以通过我这个代码来测试你们自己写的websocket对不对,但是要注意请求路径

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Chat</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', sans-serif;
        }

        body {
            background: #f0f2f5;
            padding: 2rem;
            max-width: 800px;
            margin: 0 auto;
        }

        .container {
            background: white;
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            padding: 2rem;
        }

        #text {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            margin-bottom: 1rem;
            font-size: 16px;
            transition: border-color 0.3s ease;
        }

        #text:focus {
            outline: none;
            border-color: #2196F3;
        }

        .button-group {
            display: flex;
            gap: 1rem;
            margin-bottom: 1.5rem;
        }

        button {
            flex: 1;
            padding: 12px 20px;
            border: none;
            border-radius: 8px;
            background: #2196F3;
            color: white;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        button:hover {
            background: #1976D2;
            transform: translateY(-1px);
        }

        button:active {
            transform: translateY(0);
        }

        #message {
            background: #f8f9fa;
            border-radius: 8px;
            padding: 1.5rem;
            height: 400px;
            overflow-y: auto;
            border: 2px solid #e9ecef;
        }

        #message div {
            margin-bottom: 0.5rem;
            padding: 8px 12px;
            background: white;
            border-radius: 6px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
        }

        /* 滚动条样式 */
        #message::-webkit-scrollbar {
            width: 8px;
        }

        #message::-webkit-scrollbar-track {
            background: #f1f1f1;
        }

        #message::-webkit-scrollbar-thumb {
            background: #c1c1c1;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <div class="container">
        <input id="text" type="text" placeholder="输入消息内容...">
        <div class="button-group">
            <button onclick="send()">发送消息</button>
            <button onclick="closeWebSocket()" style="background: #ff4444;">关闭连接</button>
        </div>
        <div id="message"></div>
    </div>
</body>
<script type="text/javascript">
    // 原有 JavaScript 代码保持不变...
    var websocket = null;

    if('WebSocket' in window){
        websocket = new WebSocket("ws://localhost:8080/message");
    } else {
        alert('Not support websocket');
    }

    websocket.onerror = function(){
        setMessageInnerHTML("连接错误");
    };

    websocket.onopen = function(){
        setMessageInnerHTML("✅ 连接成功");
    }

    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    websocket.onclose = function(){
        setMessageInnerHTML("❌ 连接已关闭");
    }

    window.onbeforeunload = function(){
        websocket.close();
    }

    function setMessageInnerHTML(innerHTML){
        const messageDiv = document.createElement('div');
        messageDiv.innerHTML = innerHTML;
        document.getElementById('message').appendChild(messageDiv);
        // 自动滚动到底部
        messageDiv.scrollIntoView({ behavior: "smooth" });
    }

    function send(){
        var message = document.getElementById('text').value;
        if(message.trim()){
            websocket.send(message);
            document.getElementById('text').value = '';
        }
    }
    
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>

五.多人在线聊天方法

我们可以将这个html页面分享给自己的好朋友,搭配着本地的服务端作为中转站能够将其看作一个小的聊天室,但是要将我们的8080端口暴露在公网上才能实现对我们本地的服务端访问,这里就要用到内网穿透方面的软件来将我们的局域网ip映射到公网ip上了,如果到时候有兴趣的朋友的话,我可以专门出一篇博客来分享我怎么进行内网穿透的方法。

最后:

本人的第二篇博客,以此来记录我的后端java学习。如文章中有什么问题请指出,非常感谢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值