Springboot实现WebSocket系列:从一个简单的例子入门

2022-08-15 09:05:25

简介

本文介绍使用Springboot简单实现Websocket的完整流程。由于是入门教程,所以以一个简单的案例呈现,包括了服务端和客户端的实现,这里服务端使用Springboot搭建,而客户端使用Java来实现。

什么是Websocket

在开始之前,先简单介绍一下Websocket的相关知识。Websocket是应用层协议,传输层使用了TCP协议来实现。Websocket与HTTP协议的不同之处在于,Websocket是全双工的,支持服务器与客户端的双向通信,而HTTP协议只能由客户端向服务端发起请求来获取资源。因此,Websocket的一个常见的应用场景是服务端向多个客户端推送消息。

环境配置

笔者的开发环境配置为:

  • AdoptOpenJDK 11
  • IntelliJ IDEA 2020.1.2 x64

服务端实现

本章节介绍服务端的实现。
1、引入Jar包
首先,在IDEA中创建一个Maven项目,在pom.xml文件中引入所需要的Jar包,如下所示:

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

一个是springboot的starter,另一个是springboot内部集成的websocket。
2、Springboot集成的Websocket
使用Springboot集成的websocket需要使用到以下几个类/接口:

  1. WebSocketHandler:WebSocket消息以及生命周期事件的处理器。
  2. WebSocketConfigurer:对WebSocket进行配置,包括配置拦截器、配置接口地址。
  3. HttpSessionHandshakeInterceptor:拦截器,可以对Websocket通信过程中的请求进行拦截处理。

除了上述三个官方提供的类/接口之外,我们还需要实现一个WebSocket的包装类,用于为每一个WebSocket实例添加额外的信息,比如客户端的ID。在Spring内,WebSocket实例以WebSocketSession形式存在,每个session都代表了一个服务端与客户端的会话。
2.1、创建WebSocket包装类

import org.springframework.web.socket.WebSocketSession;publicclassWebSocketBean{private WebSocketSession webSocketSession;privateint clientId;publicintgetClientId(){return clientId;}publicvoidsetClientId(int clientId){this.clientId= clientId;}public WebSocketSessiongetWebSocketSession(){return webSocketSession;}publicvoidsetWebSocketSession(WebSocketSession webSocketSession){this.webSocketSession= webSocketSession;}}

这里的包装类很简单,仅添加了一个客户端ID的属性,这里仅作为简单的示例。

2.2、实现WebSocketHandler接口
WebSocketHandler接口用于处理WebSocket的消息,Spring提供了一个抽象类AbstractWebSocketHandler实现了WebSocketHandler接口,因此我们可以直接继承抽象类,重写需要实现的方法即可。

import org.slf4j.Logger;import org.slf4j.LoggerFactory;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.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicInteger;publicclassMyWebsocketHandlerextendsAbstractWebSocketHandler{privatefinal Logger logger= LoggerFactory.getLogger(getClass());privatestaticfinal Map<String, WebSocketBean> webSocketBeanMap;privatestaticfinal AtomicInteger clientIdMaker;//仅用用于标识客户端编号static{
        webSocketBeanMap=newConcurrentHashMap<>();
        clientIdMaker=newAtomicInteger(0);}@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throws Exception{//当WebSocket连接正式建立后,将该Session加入到Map中进行管理
        WebSocketBean webSocketBean=newWebSocketBean();
        webSocketBean.setWebSocketSession(session);
        webSocketBean.setClientId(clientIdMaker.getAndIncrement());
        webSocketBeanMap.put(session.getId(), webSocketBean);}@OverridepublicvoidafterConnectionClosed(WebSocketSession session, CloseStatus status)throws Exception{//当连接关闭后,从Map中移除session实例
        webSocketBeanMap.remove(session.getId());}@OverridepublicvoidhandleTransportError(WebSocketSession session, Throwable exception)throws Exception{//传输过程中出现了错误if(session.isOpen()){
            session.close();}
        webSocketBeanMap.remove(session.getId());}@OverrideprotectedvoidhandleTextMessage(WebSocketSession session, TextMessage message)throws Exception{//处理接收到的消息
        logger.info("Received message from client[ID:"+ webSocketBeanMap.get(session.getId()).getClientId()+"]; Content is ["+ message.getPayload()+"].");
        TextMessage textMessage=newTextMessage("Server has received your message.");
        session.sendMessage(textMessage);}}

实现的逻辑很简单,在每个方法都有了注释。值得注意的是,这里在handleTextMessage处理接收到的消息,表示处理接收到的字符串消息。除此之外,AbstractWebSocketHandler还提供了handleBinaryMessage以及handlePongMessage,前者表示处理二进制消息,而后者表示处理心跳数据包的信息。

2.3、实现WebSocket拦截器
WebSocket拦截器可以在Websocket连接建立之前的权限校验等功能,Spring提供了HttpSessionHandshakeInterceptor这个接口作为拦截器。

import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.http.server.ServletServerHttpRequest;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;import java.util.Map;publicclassMyWebSocketInterceptorextendsHttpSessionHandshakeInterceptor{privatefinal Logger logger= LoggerFactory.getLogger(getClass());@OverridepublicbooleanbeforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes)throws Exception{
        logger.info("[MyWebSocketInterceptor#BeforeHandshake] Request from "+ request.getRemoteAddress().getHostString());if(requestinstanceofServletServerHttpRequest){
            ServletServerHttpRequest serverHttpRequest=(ServletServerHttpRequest) request;
            String token= serverHttpRequest.getServletRequest().getHeader("token");//这里做一个简单的鉴权,只有符合条件的鉴权才能握手成功if("token-123456".equals(token)){returnsuper.beforeHandshake(request, response, wsHandler, attributes);}else{returnfalse;}}returnsuper.beforeHandshake(request, response, wsHandler, attributes);}@OverridepublicvoidafterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex){
        logger.info("[MyWebSocketInterceptor#afterHandshake] Request from "+ request.getRemoteAddress().getHostString());}}

从代码可以看出,笔者在握手之前对请求进行了一个简单的校验,符合条件的请求才会进行下一步的握手。

2.4、对WebSocket进行配置
该步骤实现Springboot对WebSocket的支持,包括了配置接口地址、配置拦截器等。

import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;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 org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration@EnableWebSocketpublicclassWebSocketConfigurationimplementsWebSocketConfigurer{@Beanpublic MyWebSocketInterceptorwebSocketInterceptor(){returnnewMyWebSocketInterceptor();}@Beanpublic ServerEndpointExporterserverEndpointExporter(){returnnewServerEndpointExporter();}@OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry){
        webSocketHandlerRegistry.addHandler(newMyWebsocketHandler(),"/websocket").addInterceptors(webSocketInterceptor());}}

从上面的代码看出,笔者将websocket的接口地址放在了“/websocket”上,当客户端访问“/websocket”时,就会尝试与服务端进行握手连接。

2.5、配置Application主类

import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.web.servlet.ServletComponentScan;@SpringBootApplication@ServletComponentScanpublicclassMainApplication{publicstaticvoidmain(String[] args){
        SpringApplication.run(MainApplication.class);}}

客户端实现

本小节介绍WebSocket的客户端实现,这里使用Java所提供的WebSocket库来实现。在IDEA中新建一个工程:WebsocketDemoClient,用来保存我们的客户端代码。在pol.xml文件中引入:

<dependencies><dependency><groupId>org.java-websocket</groupId><artifactId>Java-WebSocket</artifactId><version>1.5.1</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>1.7.25</version><scope>compile</scope></dependency></dependencies>

1、创建Websocket client
WebsocketClient提供了连接到服务端的WebSocket的一切必要准备,我们只需要继承该类,重写所需要的方法来实现我们的业务逻辑,就能方便地使用WebSocket了。

import org.java_websocket.client.WebSocketClient;import org.java_websocket.handshake.ServerHandshake;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.net.URI;publicclassMyWebSocketClientextendsWebSocketClient{private Logger logger= LoggerFactory.getLogger(getClass());publicMyWebSocketClient(URI serverUri){super(serverUri);}@OverridepublicvoidonOpen(ServerHandshake serverHandshake){
        logger.info("[MyWebSocketClient#onOpen]The WebSocket connection is open.");}@OverridepublicvoidonMessage(String s){
        logger.info("[MyWebSocketClient#onMessage]The client has received the message from server."+"The Content is ["+ s+"]");}@OverridepublicvoidonClose(int i, String s,boolean b){
        logger.info("[MyWebSocketClient#onClose]The WebSocket connection is close.");}@OverridepublicvoidonError(Exception e){
        logger.info("[MyWebSocketClient#onError]The WebSocket connection is error.");}}

从上述代码可以看出,实现逻辑很简单,这里主要是打印出各个方法执行时的情况。

2、使用WebSocketClient
下面创建Main函数来使用我们的WebSocketClient客户端,同时创建一个定时任务,使得客户端可以连续不间断地给服务端发送消息,代码如下所示:

import java.net.URI;import java.util.Timer;import java.util.TimerTask;import java.util.concurrent.atomic.AtomicInteger;publicclassMain{privatestaticfinal AtomicInteger count=newAtomicInteger(0);publicstaticvoidmain(String[] args){
        URI uri= URI.create("ws://127.0.0.1:8080/websocket");//注意协议号为ws
        MyWebSocketClient client=newMyWebSocketClient(uri);
        client.addHeader("token","token-123456");//这里为header添加了token,实现简单的校验try{
            client.connectBlocking();//在连接成功之前会一直阻塞

            Timer timer=newTimer();
            MyTimerTask timerTask=newMyTimerTask(client);
            timer.schedule(timerTask,1000,2000);}catch(InterruptedException e){
            e.printStackTrace();}}staticclassMyTimerTaskextendsTimerTask{privatefinal MyWebSocketClient client;publicMyTimerTask(MyWebSocketClient client){this.client= client;}@Overridepublicvoidrun(){
            client.send("Test message from client, the number is "+ count.getAndIncrement());}}}

运行结果

服务端和客户端的代码编写完毕后,我们来看一下结果。先运行服务端代码,将服务端应用程序部署于8080端口中,然后运行客户端代码,结果分别如下图所示:
服务端运行结果
客户端运行结果
OK,运行结果正确,客户端与服务端建立了WebSocket链路,并实时发送消息到服务端中。本文所介绍的简单的例子到此为止,感谢你的阅读~

  • 作者:程序员的自我反思
  • 原文链接:https://blog.csdn.net/a553181867/article/details/115755554
    更新时间:2022-08-15 09:05:25