关注

手把手带你吃透Java中的WebSocket,纯干货不废话!

手把手带你吃透Java中的WebSocket,纯干货不废话!

一、从 “小麻烦” 引出 WebSocket

在这里插入图片描述

在互联网的世界里,HTTP 协议就像是一个勤劳的 “快递员”,一直勤勤恳恳地为客户端和服务器传递着信息。多年来,HTTP 协议凭借着简单、灵活的特性,成为了 Web 通信的基石,像我们日常上网浏览网页、提交表单等操作,背后都离不开 HTTP 协议的支持。它采用请求 - 响应的模式,客户端发起请求,服务器返回响应,这种模式就好比你在网上购物,下单(发送请求)后等待商家发货(返回响应),简单直接,在大多数情况下都能很好地满足我们的需求。

不过,时代在发展,互联网应用也越来越丰富多样。就像你现在不满足于只是逛逛静态网页,还想和朋友来一场畅快淋漓的在线聊天,或者实时查看股票行情的变化。这时候,HTTP 协议这个 “老快递员” 就有点力不从心了。因为 HTTP 协议是单向通信的,它只能被动地等待客户端发起请求,然后再返回响应。这就意味着,如果我们想要实现实时通信的功能,比如在线聊天,就只能让客户端不停地向服务器发送请求,询问是否有新消息,就像一个急性子的人不停地问 “到了没?到了没?”,这种方式被称为轮询。

轮询又分为短轮询和长轮询。短轮询就像是每隔几分钟就去问一次,不管有没有新消息,它都坚持不懈地问。这样做的缺点很明显,会产生大量无用的请求,浪费网络带宽和服务器资源,而且数据的实时性也很差,因为客户端要等下一次请求才能获取到新消息。长轮询呢,稍微聪明一点,客户端发起请求后,服务器如果没有新消息,就会把请求挂起,等有消息了再返回响应。但它也有问题,服务器资源消耗大,而且处理数据更新频繁的情况时也很吃力。

举个例子,假设你在一个在线股票交易平台上,想要实时跟踪股票价格的变化。如果使用 HTTP 轮询的方式,客户端每隔一段时间就向服务器发送请求获取股票价格。在股票价格波动不频繁的时候,这种方式可能还能勉强应付,但一旦股票价格快速变化,比如在股市开盘和收盘的高峰期,大量的轮询请求会让服务器不堪重负,同时你看到的股票价格也会因为请求的延迟而不够实时,这对于需要及时做出交易决策的你来说,无疑是个巨大的困扰。又比如在在线聊天应用中,你和朋友聊天,每次发送消息后都要等待客户端下一次轮询才能收到对方的回复,这聊天体验简直糟糕透顶,就像两个人打电话,说一句等半天才能听到对方回应,完全没有聊天的流畅感。

为了解决 HTTP 协议在实时通信方面的这些 “小麻烦”,WebSocket 协议应运而生,它就像是 HTTP 协议的升级版 “超级快递员”,不仅能送货(传输数据),还能主动上门给你送最新消息,实现了客户端和服务器之间的双向实时通信,让实时通信变得更加高效和流畅。接下来,就让我们一起深入了解这个神奇的 WebSocket 协议吧!

二、WebSocket 到底是何方神圣

(一)WebSocket 定义

WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通信的协议 。啥叫全双工通信呢?简单来说,就是客户端和服务器之间可以同时进行双向的数据传输。以前用 HTTP 协议的时候,就像你和朋友打电话,得一个说完另一个再说(单向通信),而现在有了 WebSocket,就好比你们用上了对讲机,随时都能畅所欲言,两边都能同时收发消息,这数据交互的效率一下子就提高了不少。有了它,客户端和服务器之间的数据交换变得更加简单直接,允许服务端主动向客户端推送数据 ,真正实现了实时通信的自由。

(二)与 HTTP 的爱恨情仇

HTTP 协议大家都很熟悉了,它是一种经典的请求 - 响应式的协议 。客户端发起请求,服务器返回响应,每次请求 - 响应完成后,连接就会关闭(短连接),除非使用了Keep - Alive机制。这就像是你去商店买东西,每次都得进店(发起请求),告诉店员你要啥(请求内容),店员给你拿(返回响应),然后你离开商店(关闭连接),下次再买还得重复这个过程。

而 WebSocket 呢,它建立的是持久连接 ,就好比你在商店办了个 VIP,直接在店里有了自己的专属座位,不用每次都进进出出,坐在那就能随时和店员交流(双向实时通信),想要啥直接说,店员有新货也能马上告诉你。从通信方向来看,HTTP 基本是单向的,主要由客户端发起请求;WebSocket 则是双向的,服务器和客户端都能主动发送数据 。在消息推送能力上,HTTP 如果要实现服务器向客户端推送消息,得借助轮询、长轮询等不太优雅的方式,不仅效率低还浪费资源;而 WebSocket 则天生就支持服务器主动推送消息,能够实时地将最新的数据推送给客户端。

总结一下,HTTP 就像是个传统的小商店,按部就班地接待顾客;WebSocket 则像是现代化的线上商城,随时能和顾客互动,提供最新的商品信息。在实时通信这个赛道上,WebSocket 的优势就凸显出来啦。

(三)工作原理大揭秘

  1. 握手阶段:WebSocket 连接的建立是从一个 HTTP 请求开始的,这个过程被称为 “握手” 。客户端发送一个带有特殊头部信息的 HTTP 请求给服务器,告诉服务器它想要升级到 WebSocket 协议。比如说下面这个请求:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

这里面Upgrade: websocketConnection: Upgrade表示客户端希望将协议升级为 WebSocket ;Sec-WebSocket-Key是一个随机生成的 Base64 编码的字符串,用于安全校验,防止被恶意篡改;Sec-WebSocket-Version指定了客户端支持的 WebSocket 协议版本,目前常用的是 13。

服务器收到请求后,如果同意建立 WebSocket 连接,就会返回一个 HTTP 101 Switching Protocols 响应 ,内容类似下面这样:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

其中Sec-WebSocket-Accept是服务器根据客户端发送的Sec-WebSocket-Key计算出来的值,用于确认服务器确实收到了客户端的请求并且同意建立连接 。计算方法是将Sec-WebSocket-Key与一个固定的 GUID(“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”)连接起来,然后计算 SHA - 1 哈希值,最后将哈希值进行 Base64 编码 。

2. 建立持久连接:一旦客户端收到服务器的 101 响应,WebSocket 连接就建立成功啦 ,这时候就从 HTTP 协议切换到了 WebSocket 协议,开始了愉快的双向通信之旅。这个连接会一直保持,除非客户端或服务器主动关闭它 ,就像你和朋友连上了对讲机,只要不主动关机,就能一直聊天。

3. 双向通信:连接建立后,客户端和服务器之间就可以通过发送和接收数据帧(Frame)来进行双向通信了 。数据帧是 WebSocket 通信的基本单位,它包含了一些控制信息和实际的数据内容 。比如说,一个简单的文本帧可能长这样:

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking - key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking - key (continued)     |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

其中FIN表示这是否是消息的最后一个帧 ;opcode定义了帧的类型,比如 0x1 表示文本帧 ;Mask表示是否对负载数据进行掩码处理(客户端发送的帧必须掩码) ;Payload length表示负载数据的长度 ;后面就是实际的负载数据啦。通过这些数据帧的传输,客户端和服务器就能实现高效的双向通信,快速地交换各种信息 。

下面是 WebSocket 握手过程的示意图:

在这里插入图片描述

这样一来,大家对 WebSocket 的工作原理是不是就有更清晰的认识啦 ?它就像是一个神奇的桥梁,让客户端和服务器之间能够高效、实时地交流,为各种实时应用的实现提供了强大的支持 。

三、Java 中使用 WebSocket 的必备技能

(一)环境搭建不迷路

要在 Java 项目中使用 WebSocket,首先得把环境搭建好,就好比盖房子得先把地基打好。这里以 Spring Boot 项目为例,给大家讲讲如何引入 WebSocket 依赖。

如果你使用的是 Maven 构建工具,那就打开项目的pom.xml文件,在<dependencies>标签里添加下面这段依赖:

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

这就好比你告诉 Maven,你想要引入 Spring Boot 的 WebSocket 启动器,这样 Maven 就会去中央仓库把相关的依赖包下载到你的项目里。

要是你用的是 Gradle,也很简单,在build.gradle文件的dependencies闭包里加上这一行:

implementation 'org.springframework.boot:spring-boot-starter-websocket'

这样就完成了依赖的引入,是不是很简单呢 ?就像点个外卖一样,轻松搞定。引入依赖后,你的项目就有了使用 WebSocket 的 “装备”,可以开始大展身手啦 。

(二)配置 WebSocket 的正确姿势

引入依赖后,接下来就得配置 WebSocket 了。在 Spring Boot 中,配置 WebSocket 有一些小技巧,要是没掌握好,可能会遇到一些小麻烦。

有一种常见的配置方式是继承WebMvcConfigurationSupport类来配置 WebSocket。比如说下面这段代码:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig extends WebMvcConfigurationSupport {

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

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        super.addResourceHandlers(registry);
        // 配置静态资源路径,这里只是示例,实际根据需求调整
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

在这段代码里,WebSocketConfig类继承了WebMvcConfigurationSupport,并重写了addResourceHandlers方法来配置静态资源路径 ,还通过@Bean注解创建了一个ServerEndpointExporter的 Bean,用于注册 WebSocket 端点 。不过,这种方式有个小问题,继承WebMvcConfigurationSupport会关闭 Spring Boot 的自动配置功能 ,这就好比你把房子重新装修了一遍,但是把原来一些好用的家具也扔掉了,会导致很多 Spring Boot 为 MVC 提供的默认配置失效 ,比如静态资源的自动映射路径可能就不对了,这时候你就得手动去配置很多东西,有点麻烦。

那有没有更好的办法呢?当然有!我们可以不继承WebMvcConfigurationSupport,而是通过实现WebSocketMessageBrokerConfigurer接口来配置 WebSocket。看下面这个改进后的配置示例:

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 配置消息代理的前缀,客户端订阅消息时需要使用这个前缀
        config.enableSimpleBroker("/topic");
        // 设置应用目的地前缀,客户端发送消息时需要使用这个前缀
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册一个STOMP端点,允许跨域请求,这里的"/websocket-endpoint"是端点路径,可根据需求修改
        registry.addEndpoint("/websocket-endpoint").setAllowedOrigins("*").withSockJS();
    }
}

在这个配置类里,通过@EnableWebSocketMessageBroker注解启用了 WebSocket 消息代理 ,configureMessageBroker方法用于配置消息代理的前缀 ,registerStompEndpoints方法注册了一个 STOMP 端点 ,并设置了允许跨域请求,同时使用了 SockJS 作为备用方案,以兼容不支持 WebSocket 的浏览器 。这种方式既简单又灵活,还能保留 Spring Boot 的自动配置功能,是不是很棒呢 ?就像给你的房子做了一次巧妙的软装,既保留了原有的舒适,又增添了新的功能。

(三)服务端开发实战

1. 定义 Endpoint

在 Java 中使用 WebSocket,定义 Endpoint 是关键的一步。Endpoint 就像是 WebSocket 连接的 “大门”,所有的通信都从这里进出。我们可以通过两种方式来定义 Endpoint,一种是注解式,另一种是编程式。

先来说说注解式定义 Endpoint,这种方式简单直接,就像在门上贴个标签,告诉大家这是 WebSocket 的入口。看下面这个例子:

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

@Component
@ServerEndpoint("/websocket")
public class WebSocketServer {

    @OnOpen
    public void onOpen(Session session) {
        // 连接建立时的处理逻辑
        System.out.println("新的连接建立: " + session.getId());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // 接收到消息时的处理逻辑
        System.out.println("接收到客户端消息: " + message);
        // 这里可以对消息进行处理,比如回复客户端
        try {
            session.getBasicRemote().sendText("服务器已收到你的消息: " + message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @OnClose
    public void onClose(Session session) {
        // 连接关闭时的处理逻辑
        System.out.println("连接关闭: " + session.getId());
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // 发生错误时的处理逻辑
        System.out.println("发生错误: " + throwable.getMessage());
        throwable.printStackTrace();
    }
}

在这段代码里,@Component注解把这个类标记为 Spring 的组件 ,@ServerEndpoint("/websocket")注解定义了 WebSocket 的端点路径为/websocket ,就像给大门取了个地址。然后通过@OnOpen@OnMessage@OnClose@OnError注解分别定义了连接建立、接收到消息、连接关闭和发生错误时的处理方法 ,每个方法里的逻辑就像是在大门处接待客人、接收包裹、送别客人和处理突发情况。

再看看编程式定义 Endpoint,这种方式相对复杂一些,就像是自己动手搭建大门,需要更多的步骤。以 Tomcat 容器为例,你需要继承javax.websocket.Endpoint类,并实现其方法。比如下面这样:

import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;

public class ProgrammerServer extends Endpoint {

    @Override
    public void onOpen(Session session, EndpointConfig config) {
        System.out.println("有新连接啦");
        session.addMessageHandler(new MessageHandler.Whole<String>() {
            @Override
            public void onMessage(String message) {
                System.out.println("收到消息: " + message);
                try {
                    session.getBasicRemote().sendText("已收到你的消息");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Override
    public void onClose(Session session, CloseReason closeReason) {
        // 连接关闭时的逻辑
        System.out.println("连接关闭啦");
    }

    @Override
    public void onError(Session session, Throwable throwable) {
        // 错误处理逻辑
        System.out.println("发生错误: " + throwable.getMessage());
        throwable.printStackTrace();
    }
}

这里ProgrammerServer类继承了Endpoint类,重写了onOpenonCloseonError方法 ,并在onOpen方法里通过session.addMessageHandler添加了消息处理器来处理接收到的消息 。不过,使用编程式定义 Endpoint 还需要额外配置,比如在 Tomcat 中,你需要创建一个ServerApplicationConfig实体来配置端点路径 ,相对来说没有注解式那么简洁方便 ,所以在实际开发中,注解式定义 Endpoint 更为常用 。

2. 生命周期方法详解

WebSocket 的生命周期方法就像是一个人的成长历程,从出生(连接建立)到经历各种事情(消息收发),再到最后离开(连接关闭),每个阶段都有对应的方法来处理。在上面的注解式定义 Endpoint 的示例中,我们已经看到了@OnOpen@OnMessage@OnClose@OnError这几个生命周期方法的使用,下面再详细解释一下它们的作用。

@OnOpen注解标注的方法会在 WebSocket 连接建立时被调用 ,这个时候就好比你交到了一个新朋友,你们刚刚建立起联系。在这个方法里,我们可以获取到Session对象,它代表了客户端和服务器之间的会话 ,通过这个Session对象,我们可以进行一些初始化的操作,比如记录连接的信息,像示例中的System.out.println("新的连接建立: " + session.getId());就是打印出连接的 ID,方便我们追踪这个连接。

@OnMessage注解标注的方法用于处理接收到的客户端消息 ,这就像是你收到了朋友给你发的信息。这个方法接收两个参数,一个是客户端发送过来的消息内容message,另一个是Session对象 。在方法里,我们可以对消息进行处理,比如解析消息内容、根据消息执行相应的业务逻辑,然后还可以通过Session对象给客户端回复消息 ,就像示例中session.getBasicRemote().sendText("服务器已收到你的消息: " + message);,把接收到的消息再回显给客户端,告诉它消息已收到。

@OnClose注解标注的方法会在 WebSocket 连接关闭时被调用 ,这就像是你和朋友的聊天结束了,要告别了。在这个方法里,同样可以获取到Session对象 ,我们可以在这里进行一些清理工作,比如释放资源、更新在线用户列表等 ,示例中的System.out.println("连接关闭: " + session.getId());就是打印出关闭的连接 ID,方便我们了解连接的状态。

@OnError注解标注的方法用于处理连接过程中发生的错误 ,就像你和朋友聊天的时候突然遇到了信号不好或者其他问题。这个方法接收Session对象和Throwable异常对象作为参数 ,通过Throwable对象我们可以获取到错误的详细信息 ,然后可以根据错误类型进行相应的处理,比如记录错误日志、给客户端发送错误提示等 ,示例中的System.out.println("发生错误: " + throwable.getMessage());throwable.printStackTrace();就是打印出错误信息并输出堆栈跟踪,方便我们调试错误。

通过这些生命周期方法,我们可以全面地管理 WebSocket 连接的各个阶段,确保通信的稳定和可靠 。

3. 消息收发实战

在 WebSocket 中,消息收发是最核心的功能,就像聊天的过程中,你要能发送消息,也要能接收消息。

先来看服务端如何接收客户端发送的数据。在注解式定义 Endpoint 的方式中,我们已经知道@OnMessage注解标注的方法会在接收到客户端消息时被调用 ,方法的参数message就是客户端发送过来的消息内容 。比如下面这个例子,假设客户端发送的是一个 JSON 格式的消息:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

@OnMessage
public void onMessage(String message, Session session) {
    // 解析JSON格式的消息
    JSONObject jsonObject = JSON.parseObject(message);
    String name = jsonObject.getString("name");
    int age = jsonObject.getIntValue("age");
    System.out.println("接收到客户端消息,姓名: " + name + ",年龄: " + age);
    // 处理完消息后可以回复客户端
    try {
        JSONObject response = new JSONObject();
        response.put("message", "已收到你的消息,姓名: " + name + ",年龄: " + age);
        session.getBasicRemote().sendText(response.toJSONString());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在这段代码里,我们使用了 FastJSON 库来解析 JSON 格式的消息 ,从消息中获取nameage字段 ,然后进行相应的处理,最后再构造一个回复消息,通过session.getBasicRemote().sendText(response.toJSONString());发送给客户端 。

再看看服务端如何推送数据给客户端。在 WebSocket 中,发送消息主要通过Session对象的getBasicRemote()getAsyncRemote()方法来实现 。getBasicRemote()方法用于同步发送消息 ,getAsyncRemote()方法用于异步发送消息 。一般来说,如果消息量不大,对实时性要求不是特别高,可以使用同步发送;如果消息量较大或者对实时性要求较高,建议使用异步发送 。比如下面这个异步发送消息的例子:

import javax.websocket.Session;

public void sendMessageAsynchronously(String message, Session session) {
    session.getAsyncRemote().sendText(message);
}

这里sendMessageAsynchronously方法接收要发送的消息messageSession对象session ,通过session.getAsyncRemote().sendText(message);异步发送消息给客户端 。这样在发送消息的同时,不会阻塞其他线程的执行,提高了系统的性能 。

通过上面的方法,我们就可以在服务端实现高效的消息收发,让 WebSocket 通信更加顺畅 。

(四)客户端开发实战

1. 创建 WebSocket 对象

在前端使用 WebSocket,首先要创建一个 WebSocket 对象,这就像是你要和别人打电话,得先拨通对方的号码。在 JavaScript 中,创建 WebSocket 对象非常简单,使用WebSocket构造函数就可以了。看下面这个例子:

// 创建WebSocket对象,这里的ws://localhost:8080/websocket是WebSocket服务器的地址,根据实际情况修改
var socket = new WebSocket('ws://localhost:8080/websocket');

在这段代码里,new WebSocket(url)构造函数创建了一个新的 WebSocket 对象 ,url参数就是 WebSocket 服务器的地址 ,格式为ws://服务器地址:端口号/端点路径 ,如果服务器支持安全连接,也可以使用wss://协议 ,就像你打电话的时候,如果想要更安全的通话,可以使用加密的线路。

2. 事件处理

创建好 WebSocket 对象后,我们需要处理各种事件,就像你和朋友打电话,要能听到对方说话(接收消息),也要能知道电话什么时候接通(连接建立)、什么时候挂断(连接关闭),如果遇到问题还要能处理(错误处理)。

onopen事件会在 WebSocket 连接建立成功时触发 ,这就像是电话拨通了,你和对方建立了联系。在这个事件的处理函数里,我们可以进行一些初始化的操作,比如发送初始化消息给服务器。看下面这个例子:

socket.onopen = function(event) {
    console.log('WebSocket连接已建立');
    // 发送初始化消息
    socket.send('Hello, server!');
};

这里socket.onopen设置了onopen事件的处理函数 ,当连接建立成功时,会打印出WebSocket连接已建立,然后通过socket.send('Hello, server!');发送一条初始化消息给服务器 。

onmessage事件用于接收服务器发送过来的消息 ,这就像是你在电话里听到了对方说的话。在这个事件的处理函数里,我们可以对接收到的消息进行处理,比如解析消息内容、更新页面显示等。看下面这个例子:

socket.onmessage = function(event) {
    console.log('收到服务器消息: ', event.data);
    // 假设接收到的是JSON格式的消息,解析消息
    var data = JSON.parse(event.data);
    console.log('解析后的消息: ', data);
    // 根据消息内容更新页面,这里只是示例,实际根据需求实现
    document.getElementById('message-display').innerHTML = data.message;
};

这里socket.onmessage设置了onmessage事件的处理函数 ,当接收到服务器消息时,会打印出收到服务器消息: 和消息内容event.data ,然后假设接收到的是 JSON 格式的消息,使用JSON.parse(event.data)解析消息 ,最后根据解析后的消息内容更新页面上idmessage-display的元素的innerHTML

onclose事件会在 WebSocket 连接关闭时触发 ,这就像是电话挂断了,你和对方的通话结束了。在这个事件的处理函数里,我们可以进行一些清理工作,比如释放资源、更新页面状态等。看下面这个例子:

socket.onclose = function(event) {
    console.log('WebSocket连接已关闭');
    // 可以在这里进行一些清理操作,比如清除定时器
    clearInterval(timer);
};

这里 `socket.onclose

四、案例实战:打造你的实时聊天小工具

(一)需求分析

我们要打造的这个实时聊天小工具,功能不需要太复杂,但要能体现 WebSocket 的核心优势,实现基本的实时通信功能。具体来说,它需要具备以下几个功能:

  1. 用户连接管理:能够管理多个用户的连接,记录每个用户的连接状态,方便后续进行消息的发送和接收。

  2. 消息广播:当一个用户发送消息时,服务器要能将这条消息广播给所有已连接的用户,让大家都能实时看到聊天内容,就像在一个大群里聊天一样,每个人说的话大家都能听到。

  3. 简单的前端界面:有一个简单的 HTML 页面作为客户端界面,用户可以在页面上输入消息并发送,同时能实时显示接收到的消息,就像我们日常使用的聊天软件的界面一样,虽然不豪华,但很实用。

(二)服务端实现

1. 代码展示

下面是使用 Spring Boot 实现的服务端关键代码:

import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@Component
@ServerEndpoint("/chat")
public class ChatServer {

    // 使用线程安全的Set来存储所有连接的会话
    private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<>());

    @OnOpen
    public void onOpen(Session session) {
        // 新用户连接时,将会话添加到Set中
        sessions.add(session);
        System.out.println("新用户连接: " + session.getId());
    }

    @OnMessage
    public void onMessage(String message, Session sender) {
        // 接收到消息后,将消息广播给所有用户,不包括发送者本身
        for (Session session : sessions) {
            if (!session.equals(sender)) {
                try {
                    session.getBasicRemote().sendText(sender.getId() + " 说: " + message);
                } catch (IOException e) {
                    e.printStackTrace();
                    // 处理发送消息失败的情况,这里简单打印异常堆栈信息
                    try {
                        // 尝试通知发送者消息发送失败
                        sender.getBasicRemote().sendText("消息发送失败,请稍后重试");
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                    // 从会话集合中移除发送失败的会话
                    sessions.remove(session);
                }
            }
        }
    }

    @OnClose
    public void onClose(Session session) {
        // 用户断开连接时,将会话从Set中移除
        sessions.remove(session);
        System.out.println("用户断开连接: " + session.getId());
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // 发生错误时,打印错误信息
        System.out.println("发生错误: " + throwable.getMessage());
        throwable.printStackTrace();
        try {
            // 尝试通知用户发生错误
            session.getBasicRemote().sendText("发生错误: " + throwable.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 从会话集合中移除发生错误的会话
        sessions.remove(session);
    }
}
2. 代码解析
  • 用户连接管理:通过@OnOpen注解的onOpen方法,当有新用户连接时,会把该用户的Session对象添加到 sessions这个Set集合中,这样就可以方便地管理所有连接的用户 。就像你开了一个派对,每来一个客人,你就把他的名字记在一个名单上,方便后续招呼大家。

  • 消息广播实现@OnMessage注解的onMessage方法负责处理接收到的消息 。当服务器接收到某个用户发送的消息后,会遍历 sessions集合,将消息发送给除了发送者本身之外的其他所有用户 。这里使用了session.getBasicRemote().sendText()方法来发送消息 ,如果发送过程中出现IOException异常,会打印异常堆栈信息,尝试通知发送者消息发送失败,并将发送失败的会话从会话集合中移除 。这就好比在派对上,有人说了一句话,你要把这句话传给其他所有人,如果传给某个人的时候出了问题,你得想办法处理,比如告诉说话的人没传成功,然后把这个出问题的人从你的 “招呼名单” 里去掉。

  • 连接关闭处理@OnClose注解的onClose方法在用户断开连接时被调用 ,会将会话从 sessions集合中移除 ,并打印用户断开连接的信息 。就像派对结束后,客人离开,你要把他的名字从名单上划掉。

  • 错误处理@OnError注解的onError方法用于处理连接过程中发生的错误 ,会打印错误信息,并尝试通知用户发生错误,同时将发生错误的会话从会话集合中移除 。比如派对上突然出现了一些意外情况,你得告诉大家发生了什么,然后把受影响的人从你的 “管理范围” 里去掉。

(三)客户端实现

1. HTML 页面搭建

下面是客户端的 HTML 页面代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实时聊天小工具</title>
    <style>
        #message-list {
            width: 400px;
            height: 300px;
            border: 1px solid #ccc;
            overflow-y: scroll;
            margin-bottom: 10px;
            padding: 5px;
        }
        #input-message {
            width: 300px;
            padding: 5px;
            margin-right: 5px;
        }
    </style>
</head>
<body>
    <h1>实时聊天小工具</h1>
    <div id="message-list"></div>
    <input type="text" id="input-message" placeholder="请输入消息">
    <button onclick="sendMessage()">发送</button>

    <script>
        // 创建WebSocket对象,连接到服务器的/chat端点
        var socket = new WebSocket('ws://localhost:8080/chat');

        // 连接建立成功时的回调函数
        socket.onopen = function (event) {
            console.log('连接已建立');
        };

        // 接收到消息时的回调函数
        socket.onmessage = function (event) {
            var messageList = document.getElementById('message-list');
            var newMessage = document.createElement('p');
            newMessage.textContent = event.data;
            messageList.appendChild(newMessage);
            // 自动滚动到最新消息
            messageList.scrollTop = messageList.scrollHeight;
        };

        // 连接关闭时的回调函数
        socket.onclose = function (event) {
            console.log('连接已关闭');
        };

        // 发送消息的函数
        function sendMessage() {
            var inputMessage = document.getElementById('input-message');
            var message = inputMessage.value;
            if (message.trim()!== '') {
                socket.send(message);
                inputMessage.value = '';
            }
        }
    </script>
</body>
</html>
2. JavaScript 代码实现
  • 创建 WebSocket 对象:通过new WebSocket('ws://``localhost:8080/chat``')创建一个 WebSocket 对象 ,连接到本地服务器的/chat端点 ,就像你拨通了派对的 “聊天热线”。

  • 事件处理

    • onopen事件:当 WebSocket 连接建立成功时触发 ,在这个事件处理函数里,我们简单地在控制台打印连接已建立,表示已经成功连接到服务器 ,就像你拨通电话后听到对方说 “喂,能听到吗”,知道电话接通了。

    • onmessage事件:用于接收服务器发送过来的消息 。当接收到消息后,会创建一个新的<p>元素,将消息内容设置为其textContent,然后添加到idmessage-listdiv元素中 ,实现消息的显示 。并且通过messageList.scrollTop = messageList.scrollHeight;让聊天窗口自动滚动到最新消息 ,就像你在聊天软件里,有新消息时聊天窗口会自动跳到最新消息处,方便你查看。

    • onclose事件:在 WebSocket 连接关闭时触发 ,同样在控制台打印连接已关闭,表示与服务器的连接断开了 ,就像电话突然挂断了。

  • 发送消息sendMessage函数用于发送消息 。它获取idinput-message的输入框的值 ,如果输入框的值不为空,就通过socket.send(message)将消息发送给服务器 ,然后清空输入框 ,准备下一次输入 ,就像你在聊天软件里输入消息后点击发送按钮,消息发出去了,输入框也清空了,等你继续输入新内容。

(四)运行与测试

  1. 运行项目:如果你使用的是 Spring Boot 项目,直接启动 Spring Boot 应用即可 。启动成功后,服务器会在8080端口监听 WebSocket 连接 ,就像派对场地布置好了,就等客人来参加。

  2. 测试过程:打开浏览器,访问http://localhost:8080/``你的HTML页面路径(这里假设 HTML 页面放在项目的静态资源目录下 ),进入聊天页面 。在输入框中输入消息并点击发送按钮 ,消息会发送到服务器 ,服务器再将消息广播给所有连接的客户端 ,每个客户端的聊天窗口都会显示这条消息 。你可以同时打开多个浏览器窗口进行测试 ,模拟多个用户聊天的场景 ,就像派对上好多人一起聊天,你说一句我说一句,非常热闹。

通过以上步骤,我们成功地使用 WebSocket 打造了一个简单的实时聊天小工具 ,是不是很有成就感呢 ?通过这个案例,相信你对 WebSocket 在 Java 中的应用有了更深入的理解和掌握 ,快去试试吧,说不定你还能在这个基础上开发出更强大的实时应用呢 !

五、WebSocket 的 “坑” 与解决方案

(一)兼容性问题

虽然 WebSocket 在现代浏览器中得到了广泛支持,但在一些旧版本的浏览器中,它的表现可能就没那么友好了。比如 IE8 及以下版本的浏览器,根本就不认识 WebSocket 这个 “新朋友” 。这就好比你精心准备了一场派对,结果有些客人因为不认识路(不支持协议)根本来不了。

那遇到这种情况该怎么办呢?别着急,我们可以使用一些兼容性方案来解决。其中比较常用的就是 SockJS ,它就像是一个万能翻译,能在不支持 WebSocket 的浏览器和服务器之间架起一座沟通的桥梁。SockJS 会自动检测浏览器是否支持 WebSocket ,如果支持,就直接使用 WebSocket 进行通信;如果不支持,它会自动回退到其他传输方式,比如 XHR - streaming(基于 HTTP 长连接的流式传输)、iframe 等 ,而且它还能保持和 WebSocket 类似的 API,让我们在使用的时候感觉和 WebSocket 没什么两样 。

使用 SockJS 也很简单,首先你得在项目中引入 SockJS 的脚本文件 ,可以通过 CDN 的方式引入,比如:

<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>

然后在 JavaScript 代码中,使用 SockJS 创建连接 ,示例代码如下:

// 使用SockJS创建连接,这里的http://your-websocket-server/sockjs是根据实际情况修改的地址
var socket = new SockJS('http://your-websocket-server/sockjs'); 
socket.onopen = function() {
    console.log('SockJS连接已建立');
};
socket.onmessage = function(e) {
    console.log('收到消息:', e.data);
};
socket.onclose = function() {
    console.log('SockJS连接已关闭');
};
socket.send('hello world!');

这样,即使在不支持 WebSocket 的浏览器中,我们也能通过 SockJS 实现类似 WebSocket 的功能 ,让我们的应用能够兼容更多的用户 。

(二)性能优化

WebSocket 的长连接特性虽然让实时通信变得更加高效,但也带来了一些性能问题。因为服务器需要一直保持与客户端的连接,这就像是你要一直陪着很多朋友聊天,会消耗大量的服务器资源,比如内存、CPU 和文件描述符等 。特别是在高并发场景下,大量的 WebSocket 连接可能会让服务器不堪重负,就像一个人要同时应付太多的事情,很容易累垮。

为了解决这些性能问题,我们可以采取一些优化措施。比如使用连接池 ,连接池就像是一个 “人才储备库”,里面预先存放了一些已经建立好的 WebSocket 连接 。当有新的客户端请求连接时,服务器可以直接从连接池中获取一个空闲的连接给它,而不是每次都重新建立连接 ,这样可以减少连接建立的开销,提高效率 。在 Java 中,可以使用一些开源的连接池框架,如 HikariCP 来管理 WebSocket 连接池 。

还有心跳检测机制也很重要 ,心跳检测就像是你和朋友聊天的时候,时不时问一句 “你还在吗?”,用来检测连接是否还正常。服务器和客户端可以定期互相发送心跳消息 ,如果一方在一定时间内没有收到对方的心跳消息,就认为连接已经断开,然后进行相应的处理,比如关闭连接、释放资源等 。这样可以及时清理无效的连接,避免资源浪费 。下面是一个简单的心跳检测的示例代码(以服务端为例):

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@ServerEndpoint("/websocket")
public class WebSocketHeartbeat {

    private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    private Session session;
    private static final int HEARTBEAT_INTERVAL = 10; // 心跳间隔时间,单位秒

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        // 启动心跳检测任务
        scheduler.scheduleAtFixedRate(new HeartbeatTask(), 0, HEARTBEAT_INTERVAL, TimeUnit.SECONDS); 
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // 接收到消息时,可以重置心跳检测的时间,比如重新设置定时器
    }

    @OnClose
    public void onClose(Session session) {
        // 关闭连接时,取消心跳检测任务
        scheduler.shutdown(); 
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // 错误处理
    }

    private class HeartbeatTask implements Runnable {
        @Override
        public void run() {
            try {
                session.getBasicRemote().sendText("heartbeat");
            } catch (IOException e) {
                e.printStackTrace();
                // 如果发送心跳消息失败,可能连接已断开,进行相应处理
            }
        }
    }
}

通过连接池和心跳检测等优化措施,可以有效地提高 WebSocket 应用的性能和稳定性 ,让服务器能够更好地应对大量的连接请求 。

(三)安全性问题

在享受 WebSocket 带来的便捷时,我们也不能忽视它可能面临的安全风险 。比如跨站 WebSocket 劫持(CSWSH) ,这就像是一个小偷趁你不注意,偷偷拿着你的身份信息去和服务器建立 WebSocket 连接,然后进行一些非法操作 。还有注入攻击,攻击者可能会往 WebSocket 发送的消息中注入恶意代码 ,如果服务器没有正确处理,就可能导致安全漏洞 。

为了防范这些安全风险,我们要采取一些措施。首先,要使用安全的 WebSocket 连接 ,也就是使用wss://协议,而不是ws://wss://协议会对数据进行加密传输 ,就像给你的快递加上了一把锁,防止数据在传输过程中被窃取或篡改 。其次,要对客户端进行身份认证 ,只有通过认证的客户端才能建立 WebSocket 连接 ,比如可以使用 JWT(JSON Web Token)进行身份验证 ,服务器在接收到连接请求时,验证 JWT 的有效性,确认客户端的身份 。

对于消息内容,也要进行严格的过滤和校验 ,防止注入攻击 。比如在 Java 中,可以使用正则表达式对接收到的消息进行校验 ,确保消息内容符合预期 ,示例代码如下:

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.regex.Pattern;

@ServerEndpoint("/websocket")
public class WebSocketSecurity {

    private static final Pattern VALID_MESSAGE_PATTERN = Pattern.compile("^[a-zA-Z0-9\\s]+$");

    @OnMessage
    public void onMessage(String message, Session session) {
        if (!VALID_MESSAGE_PATTERN.matcher(message).matches()) {
            // 如果消息不符合规则,拒绝处理并返回错误信息
            try {
                session.getBasicRemote().sendText("非法消息内容");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return;
        }
        // 处理合法消息
    }
}

通过这些安全防范措施,可以大大提高 WebSocket 应用的安全性 ,让我们的实时通信更加可靠 。

六、总结与展望

WebSocket 作为一种高效的实时通信协议,凭借其全双工通信、低延迟、持久连接等特性,为现代 Web 应用的实时交互提供了强大的支持 。在 Java 开发中,通过简单的依赖引入和配置,我们就能轻松地使用 WebSocket 构建各种实时应用 ,从在线聊天工具到实时数据监控系统,它的应用场景十分广泛 。

回顾我们在 Java 中使用 WebSocket 的过程,从环境搭建、配置、服务端和客户端开发,再到案例实战和问题解决 ,每一步都让我们对 WebSocket 有了更深入的理解和掌握 。通过打造实时聊天小工具这个案例,我们不仅学会了如何将 WebSocket 应用到实际项目中,还体会到了它在实现实时通信时的便捷和高效 。同时,我们也了解到在使用 WebSocket 过程中可能遇到的兼容性、性能和安全等问题,并掌握了相应的解决方案 ,这些知识和经验将对我们今后的开发工作大有帮助 。

随着互联网技术的不断发展,实时通信的需求将会越来越大 。WebSocket 有望在更多领域得到应用和拓展 ,比如在智能家居领域,通过 WebSocket 可以实现手机与智能家电之间的实时通信 ,用户可以随时随地控制家电设备 ;在在线教育平台中,WebSocket 可以实现老师和学生之间的实时互动,如实时答疑、在线讨论等 ,提升教学效果 ;在金融领域,实时股票行情、交易信息的推送也离不开 WebSocket 的支持 ,让投资者能够及时获取最新的市场动态 。

如果你对 WebSocket 感兴趣,不妨深入学习相关知识 ,尝试在自己的项目中使用它 。相信你会发现 WebSocket 的更多魅力和潜力 ,为用户带来更加流畅、高效的实时交互体验 。让我们一起期待 WebSocket 在未来的互联网世界中绽放更加耀眼的光芒 !

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/m0_73590302/article/details/150262572

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--