《知识点扫盲 · 学会 WebSocket》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

CSDN.gif

写在前面的话

本系列的上篇文章《知识点扫盲 · 学会 WebService》介绍了企业开发中WebService技术的实际应用,这边继续介绍一下WebSocket的基础应用,希望可以帮助到大家。
这里先介绍实战运用,深入的部分后续专题介绍,让我们开始!

Tips:WebSocket,总感觉名字和 WebService 怎么那么像?WS又算谁的缩写呢?


长连接 WebSocket

技术简介

1、WebSocket 是 HTML5 开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。
2、WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
3、WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
4、虽然前后端都可以互相推数据,但主要还是后端推送给前端,常见运用场景:实时聊天、通知公告、视频弹幕。

Tips:总结一句话,保持长连接,让前后端可以互相交互,畅通无阻。

四个事件

open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

Tips:混个眼熟。

SpringBoot 整合 WS

Step1、引入依赖

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

Step2、添加配置类

@Configuration
public class WebSocketConfig {

    /**
     * 	注入ServerEndpointExporter,
     * 	这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

Step3、添加具体Socket服务

@Component
@Slf4j
@ServerEndpoint("/webSocket/{userId}")
public class WebSocketHandle {

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     * 虽然@Component默认是单例模式的,但SB还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
     */
    private static final CopyOnWriteArraySet<WebSocketHandle> WEB_SOCKETS = new CopyOnWriteArraySet<>();

    /**
     * 用来存在线连接用户信息
     */
    private static final ConcurrentHashMap<String, Session> SESSION_POOL = new ConcurrentHashMap<>();

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        WebSocketHandle that = (WebSocketHandle) o;
        return Objects.equals(session, that.session) && Objects.equals(userId, that.userId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(session, userId);
    }

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {

        //初始化设置当前实例的信息
        this.session = session;
        this.userId = userId;

        //加入全局Socket管理(Set)
        WEB_SOCKETS.add(this);

        //加入全局session管理(Map)
        SESSION_POOL.put(userId, session);

        log.info("WebSocket有新的连接,用户为:{}, 总数为:{}", userId, WEB_SOCKETS.size());
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        WEB_SOCKETS.remove(this);
        SESSION_POOL.remove(this.userId);
        log.info("WebSocket有连接断开,用户为:{}, 总数为:{}", userId, WEB_SOCKETS.size());
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(Session session, String message) {
        log.info("WebSocket收到客户端消息,用户为:{}, 消息为:{}:", this.userId, message);
    }

    /**
     * 发送错误时的处理
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误,原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 发消息给全部人
     */
    public void sendAllMessage(String message) {
        log.info("【websocket消息】广播消息:" + message);
        for (WebSocketHandle webSocket : WEB_SOCKETS) {
            try {
                if (webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote()
                            .sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发消息给单个人
     */
    public void sendOneMessage(String userId, String message) {
        Session session = SESSION_POOL.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("【websocket消息】 单点消息:" + message);
                session.getAsyncRemote()
                        .sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发消息给多个人
     */
    public void sendMoreMessage(String[] userIds, String message) {
        for (String userId : userIds) {
            Session session = SESSION_POOL.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【websocket消息】 单点消息:" + message);
                    session.getAsyncRemote()
                            .sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Step4、测试服务端效果
参考:WebSocket在线测试
按上述步骤改造完,就可以得到WS的服务端,地址形如:ws://127.0.0.1:8180/webSocket/123
如果想不写客户端,可以直接使用测试网站进行测试,如下图。
image.png

Step5、测试发送消息
随便写一个接口,就可以触发调用了,看看客户端是否有接收到。
注意,这里的webSocketHandle确实是单例的,但是单个Socket连接也是存在的,而且Bean实例里面的static类型的Map和Set还是存了后续想要操作的内容。

@RequestMapping("/socketTest")
public void socketTest(String id) throws Exception {
    webSocketHandle.sendOneMessage(id, "hello~");
}

前端 JS 实现 WS

前端使用WebSocket,可以用原生的方式,参考如下。
当然,也可以引用第三方库,比较出名的是socket.io。

/**
 * 备忘
 * 页面地址:http://localhost:8180/test/socketDemo.html
 * 后端发消息:http://localhost:8180/socketTest?id=123
 */

if ("WebSocket" in window) {

    let $scope = {}
    let wsUrl = "ws://127.0.0.1:8180/webSocket/123"
    let ws;
    let tt;
    let lockReconnect = false;

    // 创建WS链接
    $scope.createWebSocket = function (wsUrl) {
        try {
            ws = new WebSocket(wsUrl);
            $scope.webSocketInit();
        } catch (e) {
            lockReconnect = false
            $scope.webSocketReconnect(wsUrl)//重连函数
        }
    };

    // 初始化WS的方法
    $scope.webSocketInit = function () {

        ws.onclose = function (error) {
            //连接关闭的回调函数,进行重连
            console.log("连接已关闭...", error);
            $scope.webSocketReconnect(wsUrl)
        };

        ws.onerror = function (error) {
            //连接错误的回调函数,进行重连
            console.log("连接错误...", error);
            $scope.webSocketReconnect(wsUrl)
        };

        ws.onopen = function () {//连接建立
            //发一个初始化连接消息
            ws.send('初始化连接');
            //启动心跳检测
            $scope.heartCheck.start();
        };

        ws.onmessage = function (event) {
            if(event.data !== 'pong'){
                let $test = $('#textA')
                let temp = $test.text();
                $test.text(temp + event.data + "\r\n");
                console.log("收到后端的消息:", event.data);
            } else {
                console.log('收到pong消息,连接还正常~')
            }

            //接收一次后台推送的消息,即进行一次心跳检测重置
            $scope.heartCheck.reset();
        };
    };

    $scope.webSocketReconnect = function (url) {
        console.log("socket 连接断开,正在尝试重新建立连接");

        //TODO 下面这段代码会导致只重连一次,后续改进了再开放
        //TODO 长时间如果没收到pong消息应该也要处理,提示一下报错之类的
        /*if (lockReconnect) {
            return;
        }
        lockReconnect = true;*/
        //没连接上会一直重连,设置延迟,避免请求过多
        tt && clearTimeout(tt);
        tt = setTimeout(function () {
            $scope.createWebSocket(url);
        }, 4000)
    };

    //心跳检测
    //onopen连接上,就开始start及时,如果在定时时间范围内,onmessage获取到了服务端消息,就重置reset倒计时,距离上次从后端获取消息30秒后,执行心跳检测,看是不是断了。
    $scope.heartCheck = {
        timeout: 5000, //默认30秒
        timeoutObj: null,
        reset: function () { //接收成功一次推送,就将心跳检测的倒计时重置为30秒
            clearTimeout(this.timeoutObj);//重置倒计时
            this.start();
        },
        start: function () {//启动心跳检测机制,设置倒计时30秒一次
            this.timeoutObj = setTimeout(function () {
                //启动心跳
                ws.send("ping");
            }, this.timeout)
        }
    };

    //开始创建webSocket连接
    $scope.createWebSocket(wsUrl);

    // 点击发消息给后端,后端收到后回复信息
    function sendMessage() {
        let message = document.getElementById("messageInput").value;
        if (message) {
            ws.send(message);
        }
    }

} else {
    // 浏览器不支持 WebSocket
    alert("您的浏览器不支持 WebSocket!");
}

总结陈词

此篇文章介绍了WebSocket的基础应用,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

CSDN_END.gif

  • 36
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战神刘玉栋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值