springboot框架项目应用实践五(websocket实践)

1.引言

我们知道web项目是基于http协议,http是应用层的协议,它的特点是无状态,什么叫做无状态呢?从编程模型的角度看,http的请求响应模型是一请求一响应,两次请求之间毫无关系!这也是为什么会需要cookie、session这样的技术,实现用户登录记住用户状态的原因。

当然,我们今天不探讨cookie、session,我想跟你分享的是关于websocket在实际应用中的一些实践,我们来搞清楚websocket是什么?以及websocket的一些实际应用场景。

在开始websocket详细内容前,我们先来看一些你熟悉的一些应用场景

  • 扫码登录
  • 扫码支付
  • 走在吃货街道上,时不时听到商店里传出:你有新的订单了,外卖嘛对吧

这些场景我们是不是非常熟悉!比如说我一会写完文章,要通过公众号去发布,首先第一件事情就是要登录公众号,打开公众号登录页,它是这样的

你看到了,靠右边是不是有一个二维码,我只要拿出手机扫一扫该二维码,然后在手机上确认登录,我就能登录地道程序员公众号了。

比如掘金平台个人首页的小铃铛:

你看到了,一旦有朋友关注,点赞,亦或搜藏文章,都会及时收到官方通知。

要实现像这样的应用场景,你是不是有想过应该如何实现呢?让我们开始今天的分享吧!

2.案例

2.1.获取数据的推拉模式

从数据传递编程模型上来说,获取数据有两种方式

  • pull拉模式
  • push推模式

拉模式是谁需要谁主动,客户端需要数据,便主动向服务端发送获取数据的请求,等待服务端的响应;推模式则由对端主动推送,只需要告诉对端,有数据以后发给我呀!就行了。

关于拉模式,推模式,在编程实现中对应了我们熟悉的

  • 拉模式:轮询、长轮询
  • 推模式:长连接

我们看到轮询,是需要客户端主动发出请求,很有可能服务端在收到请求的时候,并没有准备好数据,于是响应客户端,你等会儿再来!客户端只能过会儿再发出请求,周而复始,来来回回浪费了不少资源,而且还不能及时获取到数据,这便是轮询的特点!我们上面举例微信公众平台扫描登录,用的即是轮询,打开浏览器检测网络请求就可以发现了。

那么长轮询呢?长轮询是当客户端发出获取数据的请求,服务端收到请求一看发现数据没准备好呢,好家伙!那我先不响应你!于是客户端收不到服务端的响应,只能等着,哪怕天荒地老也得等,一直等待服务端有了数据响应为止。我们熟悉的nacos、apollo配置中心组件,在客户端、服务端传递配置变更的时候即用了长轮询的方式。

不管轮询,还是长轮询,都是基于http,由客户端主动发起请求的编程模型,都存在获取数据实时性的问题,没有办法做到实时性。这个时候,长连接终于站了起来说,要实时性,那你选我呀!

长连接,即是要在客户端,服务端对端之间建立起连接,实现全双工的通信方式,谁都可以随时发言,还能同时发言,这也就是我们今天的主角websocket,它要做的事情。

2.2.什么是websocket

websocket是应用层的协议, 它是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议 。通过websocket客户端(浏览器)与服务端只需要完成一次握手,对端之间就可以创建持久性的连接,进行全双工通信(数据双向传递)。下图来源于菜鸟教程

从上图我们直观看到了基于http的轮询,与基于websocket的长连接通信方式上的差异。为了看到我们说的websocket一次握手,我们再看一个图

我们看到websocket,它其实是借助http实现的升级Upgrade,即一次握手的由来。

2.3.websocket应用案例

今天大多数小伙伴都是基于springboot开发应用,那么我们就来看一下在springboot中如何应用实践websocket。案例非常简单,模拟在线客户系统,实现在浏览器,与服务端的双向通信。且代码我都写了注释,非常容易看明白。

2.3.1.引入依赖
xml 复制代码
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.3.2.开启websocket支持
java 复制代码
/**
 * 启用spring boot 支持webSocket
 *
 * @author ThinkPad
 * @version 1.0
 */
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
2.3.3.编写websocket服务端
java 复制代码
/**
 * 客服服务端:WebSocket Server
 *
 * @author ThinkPad
 * @version 1.0
 */
@Component
@Slf4j
@ServerEndpoint("/kf/{sid}")
public class KfWebSocketServer {

    /**
     * 在线用户数量
     */
    private static int clientCount = 0;

    /**
     * 存放客户端连接对象
     */
    private static CopyOnWriteArraySet<KfWebSocketServer> clientSet = new CopyOnWriteArraySet<KfWebSocketServer>();

    /**
     * 客户端连接会话
     */
    private Session session;

    /**
     * 客户端标识
     */
    private String sid = "";

    /**
     * 连接建立成功调用方法
     * @param session
     * @param sid
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        this.sid = sid;

        // 将客户端连接,放入集合
        clientSet.add(this);
        // 客户端连接数 计数加一
        addClientCount();
        log.info("新客户建立连接标识:{},当前在线人数:{}", sid, getClientCount());
    }

    /**
     * 连接关闭调用方法
     */
    @OnClose
    public void onClose() {
        // 从集合中移除当前客户端
        clientSet.remove(this);
        // 在线客户端数 计数减一
        subClientCount();
        log.info("客户端连接断开标识:{},当前在线人数:{}", sid, getClientCount());
    }

    /**
     * 收到客户端信息调用方法
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自客户消息标识:{},消息:{}", sid, message);
        try {
            sendMessage("来自服务端响应:" + message);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 服务端、客户端连接发生错误
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 服务端推送消息
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 将消息推送给指定客户端
     * @param message
     * @param sid
     * @throws IOException
     */
    public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
        for (KfWebSocketServer item : clientSet) {
            try {
                if (item.sid.equals(sid)) {
                    item.sendMessage("来自服务端响应:" + message);
                    break;
                }
            } catch (IOException e) {
                continue;
            }
        }
    }

    /**
     * ===============================================================
     */
    public static synchronized int getClientCount() {
        return clientCount;
    }

    public static synchronized void addClientCount() {
        KfWebSocketServer.clientCount++;
    }

    public static synchronized void subClientCount() {
        KfWebSocketServer.clientCount--;
    }

}
2.3.4.编写controller
java 复制代码
/**
 * 客服controller
 *
 * @author ThinkPad
 * @version 1.0
 */
@Controller
@RequestMapping("/kf")
public class KfController {

    /**
     * 进入客户主页面
     * @param userId
     * @return
     */
    @GetMapping("/index/{userId}")
    public ModelAndView index(@PathVariable String userId) {
        ModelAndView mav = new ModelAndView("/index");
        mav.addObject("userId", userId);
        return mav;
    }

    /**
     * 推送数据到客户端
     * @param cid
     * @param message
     * @return
     */
    @ResponseBody
    @RequestMapping("/push/{cid}")
    public Map push(@PathVariable String cid, String message) {
        Map<String,Object> result = new HashMap<>(16);
        try {
            KfWebSocketServer.sendInfo(message, cid);
            result.put("code", cid);
            result.put("msg", message);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }
}
2.3.5.编写前端页面
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <title>在线客服系统,欢迎您!</title>
</head>

<body>
<input type="hidden" th:value="${userId}" id="userId"/>

消息:<input id="sendMsg" type="text" />
<button onclick="send()">发送消息</button>
<hr/>
<div id="msg"></div>
</body>
<script type="text/javascript">
    // 用户Id,服务端地址
    var userId = document.getElementById("userId").value;
    var ws_address = "ws://localhost:8888/kf/" + userId;

    var kf = {};

    // 检查浏览器是否支持webSocket
    if('WebSocket' in window) {
        // 创建webSocket对象
        kf.wSocket = new WebSocket(ws_address);
    } else {
        alert('抱歉,您的浏览器不支持webSocket!')
    }

    // 回调方法:连接成功
    kf.wSocket.onopen = function (ev) {
        document.getElementById('msg').innerHTML +=  '与服务端连接成功!<br/>';
    }

    // 回调方法:连接失败
    kf.wSocket.onerror = function (ev) {
        document.getElementById('msg').innerHTML +=  '与服务端连接失败!<br/>';
    }

    // 回调方法:收到消息
    kf.wSocket.onmessage = function (ev) {
        document.getElementById('msg').innerHTML +=  ev.data + '<br/>';
    }

    // 回调方法:关闭连接
    kf.wSocket.onclose = function (ev) {
        document.getElementById('msg').innerHTML +=  '与服务端连接关闭!<br/>';
    }

    // 监听窗口关闭事件,当窗口关闭时,主动去关闭webSocket连接
    // 防止连接还没断开就关闭窗口,server端发生异常
    window.onbeforeunload = function() {
        kf.wSocket.close();
    }

    // 发送消息
    function send(){
        var message = document.getElementById('sendMsg').value;
        kf.wSocket.send(message);
    }
</script>

</html>
2.3.6.测试结果

前端

后端

相关推荐
你的人类朋友5 分钟前
✍️Node.js CMS框架概述:Directus与Strapi详解
javascript·后端·node.js
面朝大海,春不暖,花不开23 分钟前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
HelloWord~1 小时前
SpringSecurity+vue通用权限系统
vue.js·spring boot
钡铼技术ARM工业边缘计算机1 小时前
【成本降40%·性能翻倍】RK3588边缘控制器在安防联动系统的升级路径
后端
wangjinjin1801 小时前
使用 IntelliJ IDEA 安装通义灵码(TONGYI Lingma)插件,进行后端 Java Spring Boot 项目的用户用例生成及常见问题处理
java·spring boot·intellij-idea
CryptoPP2 小时前
使用WebSocket实时获取印度股票数据源(无调用次数限制)实战
后端·python·websocket·网络协议·区块链
白宇横流学长2 小时前
基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
java·spring boot·后端
草捏子2 小时前
状态机设计:比if-else优雅100倍的设计
后端
考虑考虑4 小时前
Springboot3.5.x结构化日志新属性
spring boot·后端·spring
涡能增压发动积4 小时前
一起来学 Langgraph [第三节]
后端