手把手带你吃透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 的优势就凸显出来啦。
(三)工作原理大揭秘
- 握手阶段: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: websocket和Connection: 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类,重写了onOpen、onClose、onError方法 ,并在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 格式的消息 ,从消息中获取name和age字段 ,然后进行相应的处理,最后再构造一个回复消息,通过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方法接收要发送的消息message和Session对象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)解析消息 ,最后根据解析后的消息内容更新页面上id为message-display的元素的innerHTML 。
onclose事件会在 WebSocket 连接关闭时触发 ,这就像是电话挂断了,你和对方的通话结束了。在这个事件的处理函数里,我们可以进行一些清理工作,比如释放资源、更新页面状态等。看下面这个例子:
socket.onclose = function(event) {
console.log('WebSocket连接已关闭');
// 可以在这里进行一些清理操作,比如清除定时器
clearInterval(timer);
};
这里 `socket.onclose
四、案例实战:打造你的实时聊天小工具
(一)需求分析
我们要打造的这个实时聊天小工具,功能不需要太复杂,但要能体现 WebSocket 的核心优势,实现基本的实时通信功能。具体来说,它需要具备以下几个功能:
-
用户连接管理:能够管理多个用户的连接,记录每个用户的连接状态,方便后续进行消息的发送和接收。
-
消息广播:当一个用户发送消息时,服务器要能将这条消息广播给所有已连接的用户,让大家都能实时看到聊天内容,就像在一个大群里聊天一样,每个人说的话大家都能听到。
-
简单的前端界面:有一个简单的 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,然后添加到id为message-list的div元素中 ,实现消息的显示 。并且通过messageList.scrollTop = messageList.scrollHeight;让聊天窗口自动滚动到最新消息 ,就像你在聊天软件里,有新消息时聊天窗口会自动跳到最新消息处,方便你查看。 -
onclose事件:在 WebSocket 连接关闭时触发 ,同样在控制台打印连接已关闭,表示与服务器的连接断开了 ,就像电话突然挂断了。
-
-
发送消息:
sendMessage函数用于发送消息 。它获取id为input-message的输入框的值 ,如果输入框的值不为空,就通过socket.send(message)将消息发送给服务器 ,然后清空输入框 ,准备下一次输入 ,就像你在聊天软件里输入消息后点击发送按钮,消息发出去了,输入框也清空了,等你继续输入新内容。
(四)运行与测试
-
运行项目:如果你使用的是 Spring Boot 项目,直接启动 Spring Boot 应用即可 。启动成功后,服务器会在
8080端口监听 WebSocket 连接 ,就像派对场地布置好了,就等客人来参加。 -
测试过程:打开浏览器,访问
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



