Springboot +WebSocket学习

2021-12-08 11:43:39 浏览数 (1)

Springboot WebSocket聊天室项目

  • WebSocket介绍
  • WebSocket的特点
  • webSocket协议
  • 客户端(浏览器)实现
    • websocket对象
    • websocket事件
    • WebSocket方法
  • 服务端实现
    • 服务端如何接受客户端发送过来的数据呢?
    • 服务端如何推送数据给客户端呢?
  • 基于WebSocket的网页聊天室
    • 需求
  • 实现流程
  • 消息格式
  • 功能实现
    • 创建项目,导入相关jar包的坐标
    • 引入静态资源
    • 引入公共资源
    • 登录功能实现
    • 获取当前登录的用户名
    • 聊天室功能
  • 涉及到的知识点
    • window-onbeforeunload 的使用
    • @ServerEndpoint注解的含义
    • ServerEndpointExporter
    • @RequestParam,@PathParam,@PathVariable等注解区别
    • @ServerEndpoint注解和通过ServerEndpointConfig.Configurator实现httpsession的传递
    • window.location.reload(false);window.location.reload(true);history.Go(0)区别
  • 参考文章
  • Netty实现的参考文章
  • Stomp SockJS框架实现参考文章
  • 采坑
  • WebSocket区分不同客户端两种方法(HttpSession和@PathParam)
  • Gitee源码链接

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协议的

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

代码语言:javascript复制
GET ws://localhost/chat HTTP/1.1
Host:localhost
Upgrade:websocket
Connection:Upgrade
Sec-Websocket-Accept: dGh1IHNhbxBsZSBub25jZQ==
Sec-Websocket-Extensions:permessage-deflate

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

代码语言:javascript复制
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对象:

代码语言:javascript复制
var ws=new WebSocket(url);

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

websocket事件

WebSocket对象的相关事件

事件

事件处理程序

描述

open

websocket对象.onopen

连接建立时触发

message

websocket对象.onmessgae

客户端接收到服务端数据时触发

error

websocket对象.onerror

通信发生错误时触发

close

websocket对象.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获取异步消息发送实例

服务端代码:

代码语言:javascript复制
@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包的坐标

代码语言:javascript复制
<?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类

代码语言:javascript复制
//浏览器发送给服务器的websocket数据
@Data
public class Message
{
    private  String toName;
    private String message;
}
代码语言:javascript复制
//服务器发送给浏览器的websocket数据
@Data
public class ResultMessage 
{
    private boolean isSystem;//是否是系统消息
    private String fromName;
    private Object message;//如果是系统消息是数组
}
代码语言:javascript复制
//用于登录响应给浏览器的数据
@Data
public class Result 
{
    private boolean flag;
    private String message;
}
代码语言:javascript复制
//用来封装消息的工具类
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:使用异步进行请求发送

代码语言:javascript复制
$('#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:进行登录逻辑处理

代码语言:javascript复制
@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:页面加载完成后,发送请求获取当前登录的用户名

代码语言:javascript复制
    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中获取当前登录的用户名并响应给浏览器

代码语言:javascript复制
    @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标签触发问题)

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

@ServerEndpoint注解的含义

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

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

ServerEndpointExporter

代码语言:javascript复制
@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的传递

代码语言:javascript复制
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);
    }
}
代码语言:javascript复制
@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用来存放当前客户端连接的数据

代码语言:javascript复制
public interface EndpointConfig {
    List<Class<? extends Encoder>> getEncoders();

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

    Map<String, Object> getUserProperties();
}

ServerEndpointConfig 继承EndpointConfig

代码语言:javascript复制
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,区分不同客户端

代码语言:javascript复制
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做消息的缓存处理

0 人点赞