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 分钟前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
追逐时光者1 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友2 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧2 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧2 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
间彧3 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
brzhang4 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang4 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
Roye_ack4 小时前
【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
java·vue.js·spring boot·后端·mybatis
学编程的小鬼5 小时前
全局异常处理器
java·spring boot