springboot——springboot集成websocket实现简单的多人聊天室(代码片段)

张起灵-小哥 张起灵-小哥     2022-12-26     262

关键词:

文章目录:

1.什么是WebSocket?

2.Java中的WebSocket API

2.1 WebSocket开发中的相关注解及API方法

2.2 前端技术对WebSocket的支持

3.多人聊天室的实现源码

3.1 pom文件中添加相关依赖

2.2 在核心配置文件中配置视图解析器

2.3 加入相关静态资源文件

2.4 编写控制层controller

2.5 写一个配置类,开启SpringBoot对WebSocket的支持

2.6 写一个工具类

2.7 WebSocket的核心类

2.8 最后是我们的jsp页面

2.9 测试结果


1.什么是WebSocket?

WebSocket协议是由HTML5定义的,基于TCP协议实现的一种网络协议,通过该协议服务器可以主动向客户端发送信息;

WebSocket 协议在2008年诞生,2011年成为W3C国际标准;

我们已经有了 HTTP 协议,为什么出现一个websocket协议?

http协议是短连接,因为请求之后,都会关闭连接,下次重新请求数据,需要再次打开链接;

WebSocket协议是一种长连接,只需要通过一次请求来初始化连接,然后所有的请求和响应都是通过这个TCP连接进行通讯;

所以HTTP协议通信只能是客户端向服务器发出请求,服务器返回响应结果,HTTP 协议做不到服务器主动向客户端推送信息,而websocket能实现服务器和客户端全双工通信;

何谓全双工

信息只能单向传送为单工;信息能双向传送但不能同时双向传送称为半双工,信息能够同时双向传送则称为全双工

基本实现原理

WebSocket协议基于TCP协议实现,客户端和服务器只需要做一个握手的动作之后,形成了一条基于客户端和服务器之间的快速通道,之后客户端与服务端之间便可以进行多次数据帧双向传输;

这样实现的目的是客户端和服务器进行频繁双向通信时,可以使服务器避免频繁创建HTTP连接,节约资源,提高工作效率和资源利用率;

传统Web推送实现

在没有WebSocket协议之前,服务器如何向浏览器端推送消息?

此时,通常的实现方式是在页面通过Ajax定时轮询,比如每隔1秒中向服务器发送一次HTTP请求,询问服务器是否有新消息,服务器返回结果;

这种形式缺点很明显,浏览器需要不断的向服务器发出HTTP请求,而HTTP请求包含较长的头部,有效信息相对较少,反复的无效请求占用了大量的带宽和 CPU 资源,造成很大的浪费,所以,WebSocket 应运而生;

HTML5定义的WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯;

WebSocket协议本质上是一个基于TCP的协议,因此与HTTP协议没有什么关系;

WebSocket的特点

全双工通信,客户端和服务器可以双向平等通信;

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

数据格式比较轻量,性能开销小,通信高效;

可以发送文本,也可以发送二进制数据;

通信具有更强的实时性;

协议标识符是ws,服务器地址举个例子就是:ws://www.abc.com/some/path

而http协议的地址则是: http://......

websocket业务场景

WebSocket聊天室;

股票实时价格显示等应用;

即时通讯、游戏、可视化大屏展示等领域;

企业内部管理通讯等功能,主要通讯协议websocket;

web网页聊天、客服系统实现;

系统提醒、用户上下线提醒、客户端同步,实时数据更新,多屏幕同步,用户在线状态,消息通知,扫描二维码登录/二维码支付,弹幕、各类信息提醒,在线选座,实时监控大屏等等;


2.Java中的WebSocket API

在Java EE 7中Java语言开始支持websocket协议,Java EE 7中定义了一套Websocket API规范,也就是一系列接口,没有实现,位于包javax.websocket下,包含客户端API和服务端API,WebSocket的Java API 只是规范,具体实现需要web容器(比如tomcat就实现了Java websocket api)、Java EE服务器或者框架提供;

javax.websocket

This package contains all the WebSocket APIs common to both the client and server side.

javax.websocket.server

This package contains all the WebSocket APIs used only by server side applications.

1、Tomcat:java中的websocket实现,需要tomcat 7.0.47+以上才支持Java EE7;

2、Spring的websocket,需要Spring 4.x,所以springboot也可以用;

2.1 WebSocket开发中的相关注解及API方法

@ServerEndpoint("/websocket/uid")

申明这是一个websocket服务;

需要指定访问该服务的地址,在地址中可以指定参数,需要通过进行占位;


@OnOpen

用法:public void onOpen(Session session, @PathParam("uid") String uid) throws IOException

该方法将在建立连接后执行,会传入session对象,就是客户端与服务端建立的长连接通道,通过@PathParam获取url中声明的参数;


@OnClose

用法:public void onClose()

该方法是在连接关闭后执行;


@OnMessage

用法:public void onMessage(String message, Session session) throws IOException

该方法用于接收客户端发送的消息;

message:发来的消息数据;

session:会话对象(也是长连接通道);

发送消息到客户端;

用法:session.getBasicRemote().sendText("hello,websocket.");

通过session进行消息发送;

2.2 前端技术对WebSocket的支持

Websocket是html5规范,主流浏览器都支持;(某些老浏览器不支持)

jQuery、vueJS、React JS、angularjs等都可以支持webscoket对象;

底层是javascript支持的一个webscoket的js对象,通过这个对象可以建立websocket的连接:ws://localhost:8080/websocket/12345 


3.多人聊天室的实现源码

3.1 pom文件中添加相关依赖

    <dependencies>

        <!-- SpringBoot框架web项目起步依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- SpringBoot框架websocket起步依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!-- SpringBoot框架内嵌Tomcat对jsp的解析依赖 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <!-- lombok依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>

    </dependencies>

    <build>

        <!-- SpringBoot框架编译打包插件 -->
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>

        <!-- src/main/webapp下的jsp页面编译到META-INF/resources下才能访问 -->
        <resources>
            <resource>
                <directory>src/main/webapp</directory>
                <targetPath>META-INF/resources</targetPath>
                <includes>
                    <include>*.*</include>
                </includes>
            </resource>
        </resources>
    </build>

2.2 在核心配置文件中配置视图解析器

#配置视图解析器
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

2.3 加入相关静态资源文件

2.4 编写控制层controller

这其中只有一个请求,/chat,这个请求会跳转到我们的index.jsp页面。

package com.szh.springboot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 */
@Controller
public class ChatController 

    //声明原子变量类,确保服务端和客户端之间操作的原子性和可见性
    private AtomicInteger atomicInteger=new AtomicInteger();

    @RequestMapping("/chat")
    public String chat(Model model) 
        model.addAttribute("username","user" + atomicInteger.getAndIncrement());
        return "index";
    

2.5 写一个配置类,开启SpringBoot对WebSocket的支持

package com.szh.springboot.config;

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.server.standard.ServerEndpointExporter;

/**
 *
 */
@EnableWebSocket //开启SpringBoot对WebSocket的支持
@Configuration //声明该类是一个配置类
public class ChatConfig 

    /**
     * 配置ServerEndpointExporter的bean
     * 该Bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpoint() 
        return new ServerEndpointExporter();
    

2.6 写一个工具类

这个工具类封装了大量静态方法,供外界调用。

package com.szh.springboot.endpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 聊天室功能实现的一个工具类
 */
public class ChatUtils 

    //定义日志对象
    private static final Logger logger= LoggerFactory.getLogger(ChatUtils.class);

    //定义map集合,确保数据共享和安全,这里使用ConcurrentHashMap
    //用户名为key,session信息为value
    public static final Map<String, Session> CLIENTS=new ConcurrentHashMap<>();

    /**
     * 使用连接发送消息
     * @param session 用户的session
     * @param message 发送的消息内容
     */
    public static void sendMessage(Session session,String message) 
        if (session == null) 
            return;
        

        final RemoteEndpoint.Basic basic=session.getBasicRemote();
        if (basic == null) 
            return;
        

        try 
            basic.sendText(message);
         catch (IOException e) 
            e.printStackTrace();
            logger.error("sendMessage IOException",e);
        
    

    /**
     * 发送消息给所有人
     * @param message
     */
    public static void sendMessageAll(String message) 
        CLIENTS.forEach((sessionId,session) -> sendMessage(session,message));
    

    /**
     * 获取所有的在线用户
     */
    public static String getOnlineInfo() 
        Set<String> userNames=CLIENTS.keySet();
        if (userNames.size() == 0) 
            return "当前无人在线......";
        
        return userNames.toString() + "在线";
    

2.7 WebSocket的核心类

package com.szh.springboot.endpoint;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

/**
 * @ServerEndpoint注解中指定WebSocket协议的地址
 * @OnOpen、@OnMessage、@OnClose、@OnError注解与WebSocket中监听事件对应
 */
@Slf4j //生成一些日志代码
@Component
@ServerEndpoint("/websocket/username")
public class ChatServerEndpoint 

    /**
     * 连接建立时触发
     */
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session) 
        log.info("用户登录",username);
        String message= "用户[" + username + "]已进入聊天室!";

        //将该用户登录的消息发送给其他人
        ChatUtils.sendMessageAll(message);

        //将自己的信息添加到map集合中
        ChatUtils.CLIENTS.put(username,session);

        //获取当前的在线人数,发给自己查看
        String onlineInfo=ChatUtils.getOnlineInfo();
        ChatUtils.sendMessage(session,onlineInfo);
    

    /**
     * 客户端接收服务端发来的数据时触发
     */
    @OnMessage
    public void onMessage(@PathParam("username") String username,String message) 
        log.info("发送消息:, ",username,message);
        //广播,把消息同步给其他客户端
        ChatUtils.sendMessageAll("[" + username + "]: " + message);
    

    /**
     * 连接关闭时触发
     */
    @OnClose
    public void onClose(@PathParam("username") String username,Session session) 
        //从当前的map集合中移除该用户
        ChatUtils.CLIENTS.remove(username);

        //将该用户离线的消息通知给其他人
        ChatUtils.sendMessageAll("[" + username + "]已离线!");

        try 
            //关闭WebSocket下的该Seesion会话
            session.close();
            log.info(" 已离线......",username);
         catch (IOException e) 
            e.printStackTrace();
            log.error("onClose error",e);
        
    

    /**
     * 聊天通信发生错误时触发
     */
    @OnError
    public void onError(Session session,Throwable throwable) 
        try 
            //关闭WebSocket下的该Seesion会话
            session.close();
         catch (IOException e) 
            e.printStackTrace();
            log.error("onError Exception",e);
        
        log.info("Throwable msg " + throwable.getMessage());
    

2.8 最后是我们的jsp页面

这里简单的说一下,当我们发起controller中对应的请求之后,会跳转到这个页面,待页面加载完之后(就是执行完<script>标签之前的代码),然后走script脚本内容,下面就是ajax + jQuery了,首先是获取了websocket的请求地址,然后就是根据情况来触发 onopen、onmessage、onclose、onerror这些事件了。其实也不难理解。

<%@ page contentType="text/html;charset=utf-8" language="java" %>
<html>
<head>
    <title>SpringBoot + WebSocket + JSP</title>
    <link rel="stylesheet" href="$pageContext.request.contextPath/css/bootstrap.min.css">
    <script src="$pageContext.request.contextPath/js/jquery.min.js"></script>
    <script src="$pageContext.request.contextPath/js/bootstrap.min.js"></script>
</head>
<body style="margin: 45px">
    <h4>张起灵在线聊天室</h4>
    <div class="form-group">
        <label for="content"></label>
        <textarea id="content" readonly="readonly" cols="80" rows="15"></textarea>
    </div>
    <div class="form-group" style="margin-top: 8px">
        <textarea id="message" cols="80" rows="5" placeholder="请输入消息"></textarea>
        <div style="margin-top: 10px">
            <button id="toSend" class="btn btn-info">发送</button>
            <button id="toExit" class="btn btn-danger">离线</button>
            <input id="username" value="$username" style="display: none">
        </div>
    </div>

    <script type="text/javascript">
        $(function () 
            var ws;
            //如果浏览器支持WebSocket
            if ("WebSocket" in window) 
                var baseUrl='ws://localhost:8080/websocket/';
                var username=$('#username').val();
                ws=new WebSocket(baseUrl + username);

                //建立连接之后,触发事件
                ws.onopen=function () 
                    console.log("建立 websocket 连接......");
                ;

                //接收后台服务端的消息,触发事件
                ws.onmessage=function (event) 
                    $('#content').append(event.data + '\\n\\n');
                    console.log("接收到服务端发送的消息......" + event.data + '\\n');
                ;

                //连接关闭时,触发事件
                ws.onclose=function () 
                    $('#content').append('[' + username + ']已离线');
                    console.log("关闭 websocket 连接......");
                ;

                //发生错误时,触发事件
                ws.onerror=function (event) 
                    console.log("websocket发生错误......" + event + '\\n');
                ;
             else  //如果浏览器不支持WebSocket
                alert("很抱歉,您的浏览器不支持WebSocket!!!");
            

            //发送按钮触发的行为,客户端发送消息到服务器
            $('#toSend').click(function () 
                sendMsg();
            );
            //支持回车键发送消息
            $(document).keyup(function (event) 
                if (event.keyCode == 13) 
                    sendMsg();
                
            );

            //发送消息的函数
            function sendMsg() 
                ws.send($('#message').val());
                $('#message').val("");
            

            //离线按钮触发的行为
            $('#toExit').click(function () 
                if (ws) 
                    ws.close();
                
            )
        )
    </script>
</body>
</html>

2.9 测试结果

我这里开了三个用户,一个user0,一个user1,一个user2。

分别在浏览器中访问三次请求地址就可以开启三个用户的聊天室了。

然后我们关闭user2的窗口,或者点击离线按钮。

当user2离线之后,在user0和user1的聊天窗口中可以看到他的离线消息。

最后依次点击user1和user0的离线按钮,因为我们之前代码中用到了Slf4j,所以我们此时回到IDEA中查看控制台打印的日志信息。

 

springboot.03.springboot集成jsp(代码片段)

SpringBoot.03.SpringBoot集成jsp前言准备工作jsp集成案例集成步骤1.新建Module2.pom.xml3.Springboot03JspApplication.java4.application.yml5.index.jsp6.JspController.java7.测试问题分析解决方案1.springboot:run2.设置Workingdirectory修改jsp无 查看详情

springboot(十八):使用springboot集成fastdfs

SpringBoot(十八):使用SpringBoot集成FastDFS环境:SpringBoot最新版本1.5.9、jdk使用1.8、tomcat8.0功能:使用SpringBoot将文件上传到分布式文件系统FastDFS中。一、pom包配置<dependency><groupId>org.csource</groupId><artifactId> 查看详情

springboot入门到精通-springboot集成ssm开发项目(代码片段)

SpringBoot入门到精通系列SpringBoot入门到精通-Spring的注解编程(一)SpringBoot入门到精通-SpringBoot入门(二)SpringBoot入门到精通-Spring的基本使用(三)SpringBoot入门到精通-SpringBoot集成SSM(四)前言上一篇文章我们讲的是SpringBoot的基本用法,... 查看详情

springboot-springboot集成rabbitmq

一、SpringBoot集成RabbitMQ创建两个模块,一个命名springboot-send,一个命名springboot-receive在两个工程的pom.xml配置文件中引入AMQP依赖<dependencies><dependency><groupId>org.springframework.boot</groupId 查看详情

springboot使用·下篇(springboot集成mybatis+日志打印+mybatis-plus)(代码片段)

文章目录SpringBoot的使用SpringBoot集成MyBatis日志打印MyBatis-plus的简单搭建SpringBoot的使用我们在SpringBoot使用·上篇说到了SpringBoot注解、SpringBoot集成JDBC、SpringBoot集成druid(德鲁伊)数据源和sql监控,这篇文章我们会写到Sp... 查看详情

rocketmq集成springboot(代码片段)

RocketMQRocketMQ集成SpringBoot1.项目配置1.1新建项目1.2引入依赖2.生产者实现3.消费者实现总结RocketMQ集成SpringBoot1.项目配置1.1新建项目新建两个SpringBoot项目,项目名分别为:springboot-rocketmq-consumer、springboot-rocketmq-producter; 查看详情

#springboot集成netty(代码片段)

SpringBoot集成Netty文章目录SpringBoot集成Netty背景描述Netty与SpringBoot整合关注点Netty组件Bootstrap、ServerBootstrapChannelEventLoop、EventLoopGroupChannelHandlerChannelPipelineByteBufPom依赖Yml配置整合Netty步骤服务端客户端背景描述如果需要在Spri 查看详情

springboot使用·上篇(springboot注解+集成jdbc+集成druid(德鲁伊)+sql监控)(代码片段)

文章目录SpringBoot的使用SpringBoot注解@SpringBootApplication@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan@Configuration@Bean@ConfigurationPropertiesSpringBoot集成JDBC 查看详情

springboot介绍

SpringBoot集成教程SpringBoot介绍SpringBoot开发环境搭建(Eclipse)SpringBootHelloWorld(restful接口)例子springboot连接Mysqlspringboot配置druid连接池连接mysqlspringboot集成mybatis(1)springboot集成mybatis(2)–使用pagehelper实现分页springboot集 查看详情

springboot之集成redis:springcache+redis

前言来啦老铁!笔者学习SpringBoot有一段时间了,附上SpringBoot系列学习文章,欢迎取阅、赐教:5分钟入手SpringBoot;SpringBoot数据库交互之SpringDataJPA;SpringBoot数据库交互之Mybatis;SpringBoot视图技术;SpringBoot之整合Swagger;SpringBoot之junit单... 查看详情

springboot系列教程七:springboot集成mybatis

一.创建项目    项目名称为“springboot_mybatis_demo”,创建过程中勾选“Web”,“MyBatis”,“MySQL”,第一次创建Maven需要下载依赖包(耐心等待)    二.实现2.1创建User类1packagecom.woniu.bean;234publicclassUser{5pr... 查看详情

springboot集成elasticsearch

点击跳转 查看详情

springboot集成elasticsearch

点击跳转 查看详情

springboot集成hbase

springboot集成hbase会启动报错主要因为SpringBoot内嵌了Web容器,方便对应用进行微服务化开发和部署。所以打算将HBase的业务应用作为一个单服务进行开发和发布,其他相关的子系统通过RESTfulAPI来访问。搭建项目环境时,需要注意... 查看详情

springboot集成mybatis通用mapper使用总结

 springboot集成MyBatis通用Mapper使用总结2019年  参考资料:Springboot集成MyBatis通用MapperSpringBoot框架之通用mapper插件(tk.mybatis)springboot如何优雅的使用mybatis-spring-boot-starter三分钟让你看懂Springboot中Mybatis的使用SpringB 查看详情

springboot学习----springboot集成mybatis

springboot作为一个一站式开发工具怎么能少了与其他框架的兼容。这里将mybatis和springboot整合起来。  1.新建一个项目  这里我们的选择如下:   如果已经有一个项目,我们可以在pom.xml文件中添加以下依赖。    <... 查看详情

springboot集成freemarker

     查看详情

springboot——springboot集成thymeleaf(代码片段)

文章目录:1.认识Thymeleaf2.详细步骤2.1创建一个SpringBoot项目2.2在pom.xml文件中会自动添加SpringBoot集成Thymeleaf的起步依赖2.3在核心配置文件中添加以下内容2.4写一个Controller控制层2.5写一个html页面2.6启动测试1.认识Thymeleaf Thymeleaf... 查看详情