Springboot +WebSocket学习

x33g5p2x  于2021-10-27 转载在 Spring  
字(11.7k)|赞(0)|评价(0)|浏览(916)

WebSocket介绍

WebSocket是一种网络通信协议,RFC6455定义了它的通信标准

WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议

HTTP协议是一种无状态的,无连接的,单向的应用层协议。

它采用了请求/响应模式,通信请求只能由客户端发起,服务端对请求做出应答处理

这种通信模型有一个弊端: HTTP协议无法实现服务器主动向客户端发起消息

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。

大多数Web应用程序将通过频繁的异步AJAX请求实现长轮询。

轮询的效率低,非常浪费资源(因为必须不停连接,获知HTTP连接始终打开)

http协议:

websocket协议:

总结:

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输

WebSocket的特点

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

总结:websocket主要是服务器主动向客户端推送消息,与客户端保持长连接,当然前提是客户端不刷新页面,否则无意义

webSocket协议

本协议有两部分:握手和数据传输

握手是基于http协议的

来自客户端的握手看起来像如下的形式:

GET ws://localhost/chat HTTP/1.1
Host:localhost
Upgrade:websocket
Connection:Upgrade
Sec-Websocket-Accept: dGh1IHNhbxBsZSBub25jZQ==
Sec-Websocket-Extensions:permessage-deflate

来自服务器的握手看起来像如下形式:

HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection:Upgrade
Sec-Websocket-Accept: s3pPLMiTxaQ9kYGzzhZRbk+xOo=
Sec-Websocket-Extensions:permessage-deflate

字段说明:

头名称说明
Connection:Upgrade标识该HTTP请求时一个协议升级请求
Upgrade:websocket协议升级为websocket协议
Sec-Websocket-Version: 13客户端支持webSocket版本
Sec-Websocket-Key客户端采用base64编码的24位随机字符序列,服务器接受客户端HTTP协议升级的证明,要求服务器端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答
Sec-Websocket-Extensions协议扩展类型

客户端(浏览器)实现

websocket对象

实现Websocket的Web浏览器将通过Websocket对象公开所有必须的客户端功能(主要指支持Html5的浏览器)

以下API用于创建Websocket对象:

var ws=new WebSocket(url);

参数url格式说明: ws://ip地址:端口号/资源名称

websocket事件

WebSocket对象的相关事件

事件事件处理程序描述
openwebsocket对象.onopen连接建立时触发
messagewebsocket对象.onmessgae客户端接收到服务端数据时触发
errorwebsocket对象.onerror通信发生错误时触发
closewebsocket对象.onclose连接关闭时触发

WebSocket方法

WebSocket对象的相关方法:

方法描述
send()使用连接发送数据
close()关闭连接

服务端实现

Tomcat的7.0.5版本开始支持WebSocket,并且实现了JAVA WebSocket规范(JSR356)

Java``WebSocket应用一系列的WebSocketEndpoint组成,EndPoint是一个java对象,代表Websocket链接的一端,对于服务端,我们可以视处理具体WebSocket消息的接口,就像servelt之与http请求一样

EndPoint和唯一个连接的客户端一一对应,例如张三登录进聊天室,那么服务端就产生一个EndPoint对象与之对应,如果有多个人登录聊天室,那么服务端就会产生多个Endpoint对象

我们可以通过两种方式定义Endpoint:

  • 第一种是编程式,即继承javax.websocket.Endpoint并实现其方法
  • 第二种是注解式,即定义一个POJO,并添加@ServerEndpoint相关注解

Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后再链接关闭时结束。

Endpoint接口中明确定义了与其生命周期相关的方法,规范实现这确保生命周期的各个阶段调用实例的相关方法。

生命周期的方法如下:

方法含义描述注解
onClose当会话关闭时调用@OnClose
onOpen当开启一个新的会话时调用,该方法是客户端与服务器端握手成功后调用的方法@OnOpen
onError当链接过程中出现异常时调用@OnError

服务端如何接受客户端发送过来的数据呢?

通过为Session添加MessageHandler消息处理器来接收消息,当采用注解方式定义Endpoint时,我们还可以通过@OnMessgae注解指定接收消息的方法

该session不是属于http里面的session对象,而是属于websocket协议里面的session对象

服务端如何推送数据给客户端呢?

发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取通过消息发送的实例,然后调用其sendXxx()方法就可以发送消息,可以通过Session.getAsyncRemote获取异步消息发送实例

服务端代码:

@ServerEndPonit("/robin")
public class ChatEndPonit
{
 private static Set<ChatEndPonit> webSocketSet=new HashSet<>();

 private Session session;

 @OnMessgae
  public void OnMessage(String messgae,Session session)
 {
     System.out.println("接收的消息是:"+messgae);
     System.out.println(session);
     //将消息发送给其他的用户
     for(Chat chat:webSocketSet)
     {
         if(chat!=this)
         {
             chat.session.getBasicRemote().sendText(messgae);
         }
     }
 }

 @OnOpen
    public void onOpen(Session session)
 {
     this.session=session;
     webSocketSet.add(this);
 }

 @OnClose
    public void onClose(Session session)
 {
     System.out.println("连接关闭了");
 }
 
 @OnError
    public void OnError(Session session,Throwable error)
 {
     System.out.println("出错了...."+error.getMessage());
 }
}

基于WebSocket的网页聊天室

需求

通过Websocket实现一个简易的聊天室功能

(1)登录聊天室

(2)登录之后,进入聊天界面进行聊天

登录成功以后,呈现出一下的效果

当我们想和李四聊天时就点击好友列表的李四,效果如下:

接下来就可以进行聊天了,张三的界面如下:

李四的界面如下:

实现流程

最后一个是OnClose,不是OnError

消息格式

  • 客户端—>服务端
    {“toName”:“张三”,“message”:“你好”}

  • 服务端---->客户端
    系统消息格式: {“isSystem”:true,“fromName”:null,“message”:{“李四”,“王五”}}
    推送消息给某一个客户端 {“isSystem”:true,“formName”:“张三”,“message”:“你好”}

功能实现

创建项目,导入相关jar包的坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.5.5</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>WebSocket</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

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

<!-- devtools热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
             <optional>true</optional>
            <scope>true</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
                <!--引入Thymeleaf模板引擎启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

     <build>
         <plugins>
<!-- 打jar包如何不配置该插件,打出来的jar包没有清单文件-->
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
         </plugins>
     </build>
</project>

引入静态资源

引入相关页面,可以自己准备

引入公共资源

pojo类

//浏览器发送给服务器的websocket数据
@Data
public class Message
{
    private  String toName;
    private String message;
}
//服务器发送给浏览器的websocket数据
@Data
public class ResultMessage 
{
    private boolean isSystem;//是否是系统消息
    private String fromName;
    private Object message;//如果是系统消息是数组
}
//用于登录响应给浏览器的数据
@Data
public class Result 
{
    private boolean flag;
    private String message;
}
//用来封装消息的工具类
public class MessageUtils
{
    public static  String getMessage(boolean isSystemMessgae,String fromName,Object message)
    {
        try {
            //服务端发送给浏览器的消息格式
            ResultMessage result=new ResultMessage();
            result.setSystem(isSystemMessgae);
            result.setMessage(message);
            //不是系统消息
            if(fromName!=null)
            {
                result.setFromName(fromName);
            }
            //Jackson的主要类
            ObjectMapper mapper=new ObjectMapper();
            //将对象转换为json字符串
            return mapper.writeValueAsString(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
}

登录功能实现

login.html:使用异步进行请求发送

$('#login-button').click(function (event) {
   //ajax发送异步登录请求
	$.ajax({
		url:"login",
		type:"post",
		data:$("form").serialize(),
		success:function (res)
		{
			if(res.flag)
			{
				//跳转到main.html页面
				location.href="main.html";
			}
			else
			{
				alert(res.message);
			}
		}
	})
	//阻止表单的默认提交行为
	return false;
});

userController:进行登录逻辑处理

@RestController
public class UserController
{
    @PostMapping("/login")
    public Result login(User user, HttpSession httpSession)
    {
        Result result=new Result();
        if(user!=null&&"123".equals(user.getPwd()))
        {
            result.setFlag(true);
            //将用户名存储到session对象中
            httpSession.setAttribute("user",user.getName());
        }
        else
        {
            result.setFlag(false);
            result.setMessage("登录失败");
        }
        return result;
    }
}

获取当前登录的用户名

main.html:页面加载完成后,发送请求获取当前登录的用户名

var username;
    $.ajax({
        url:"getUserName",
        type:"post",
        success:function (res)
        {
            username=res
            $("#userName").html("用户: "+res+"<span style='float: right;color: green'>在线</span>")
        }
        ,async:false//同步请求
    })

在userController中添加一个getUserName方法,用来从session中获取当前登录的用户名并响应给浏览器

@PostMapping("/getUserName")
    public String getUserName(HttpSession httpSession)
    {
        String username=(String)httpSession.getAttribute("user");
        return username;
    }

聊天室功能

代码太多,不方便贴出,具体可以去我的gitee仓库,下载查看

涉及到的知识点

window-onbeforeunload 的使用

window-onbeforeunload 的使用

离开页面的判断:window.Onunload与window.onbeforeunload的区别(IE下a标签触发问题)

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function() {
        ws.close();
    }

@ServerEndpoint注解的含义

@ServerEndpoint注解是服务端与客户端交互的关键,其值(/test/one)得与index页面中的请求路径对应。

(备注:服务端关闭或者浏览器关闭或者刷新的效果,都会导致连接断开)

ServerEndpointExporter

@Configuration
public class WebSocketStompConfig {
    //这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket ,如果你使用外置的tomcat就不需要该配置文件
    @Bean
    public ServerEndpointExporter serverEndpointExporter()
    {
        return new ServerEndpointExporter();
    }
}

@RequestParam,@PathParam,@PathVariable等注解区别

@RequestParam,@PathParam,@PathVariable等注解区别

@ServerEndpoint注解和通过ServerEndpointConfig.Configurator实现httpsession的传递

public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator
{
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession= (HttpSession) request.getHttpSession();
        //将httpsession对象存储到配置对象中
        //Map<String, Object> getUserProperties();
        //向map集合中存放我们需要的数据,我们可以在其他使用到ServerEndpointConfig对象的类中取出该属性
        //因为都是一个ServerEndpointConfig对象
        sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)//指定请求路径
@Component
public class chatEndPoint
{
    @OnOpen//链接建立时被调用
    public void OnOpen(Session session, EndpointConfig config)
    {
        System.out.println("=====================");
        System.out.println("连接建立");
        //将局部的sesssion对象赋值给成员session
        this.session=session;
        //获取httpsession对象
        httpSession=(HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        //从httpsession对象中获取用户名
        String username=(String) httpSession.getAttribute("user");
    }
}

什么两个地方的EndPointCong一致:

下面的EndpointConfig 是一个接口,里面定义了一个map用来存放当前客户端连接的数据

public interface EndpointConfig {
    List<Class<? extends Encoder>> getEncoders();

    List<Class<? extends Decoder>> getDecoders();

    Map<String, Object> getUserProperties();
}

ServerEndpointConfig 继承EndpointConfig

public interface ServerEndpointConfig extends EndpointConfig {}

每天注解学习(八)获取HttpSession的工具类,源码详细分析@ServerEndpoint注解

window.location.reload(false);window.location.reload(true);history.Go(0)区别

window.location.reload(false);window.location.reload(true);history.Go(0)区别

参考文章

SpringBoot集成WebSocket—基础用法演示

SpringBoot2.0集成WebSocket,实现后台向前端推送信息

关于controller调用controller/service调用service/util调用service/websocket中autowired的解决方法

SpringBoot2+WebSocket之聊天应用实战(优化版本)

WebSocket 详解教程

SpringBoot 整合WebSocket 简单实战案例

WebSocket 教程—偏向前端代码实现—阮一峰的网络日志

Netty实现的参考文章

Springboot2构建基于Netty的高性能Websocket服务器(netty-websocket-spring-boot-starter)

SpringBoot2+Netty+WebSocket(netty实现websocket,支持URL参数)

Stomp+SockJS框架实现参考文章

Spring websocket+Stomp+SockJS 实现实时通信 详解

Spring使用WebSocket、SockJS、STOMP实现消息功能

Springboot 整合Websocket+Stomp协议+RabbitMQ做消息代理 实例教程

Springboot 整合 WebSocket ,使用STOMP协议+Redis 解决负载场景问题(二)

Springboot 整合 WebSocket ,使用STOMP协议 ,前后端整合实战 (一)

采坑

使用websocket踩到的坑儿,,,

WebSocket区分不同客户端两种方法(HttpSession和@PathParam)

添加链接描述WebSocket区分不同客户端两种方法(HttpSession和@PathParam)

Websocket如何获取httpSession,区分不同客户端

public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator
{
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession= (HttpSession) request.getHttpSession();
        //将httpsession对象存储到配置对象中
        //Map<String, Object> getUserProperties();
        //向map集合中存放我们需要的数据,我们可以在其他使用到ServerEndpointConfig对象的类中取出该属性
        //因为都是一个ServerEndpointConfig对象
        sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
        super.modifyHandshake(sec, request, response);
    }
}

这里GetHttpSessionConfigurator 对于所有客户端来说只有一份

ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response传入的这三个参数对于所有客户端来说只有一份

因此这里request.getHttpSession()得到的HttpSession对于所有客户端来说也只有一份

Gitee源码链接

websocket聊天室功能简陋版

待实现功能:信息的存储,可以使用localstorage,但是不够安全,信息会丢失,更建议结合rebbitmq或者redis做消息的缓存处理

相关文章