• WebSocket理解和使用


    引言:什么是WebSocket?

    WebSocket和http一样,都是一种网络传输协议,但是和Http协议相比,它有一点不同,它可以在单个TCP连接上进行全双工通信,通俗来说就是客户端可以向服务端发送请求,服务端也可以向客户端发送请求;

    这张图网上有很多,完美展示了http和webSocket的区别:

    image-20220811224458154

    我在这里再解释一下:

    • http协议:客户端需要向服务端发送request请求,然后服务端会对该请求进行相应的处理,处理完成后响应Response到客户端;这就是一个流程;

      如果客户端不向服务端发送请求,那么服务端就不会进行响应;

    • webSocket协议:首先客户端和服务端握手之后,它们之间的通道就打通了,此时客户端依然可以向服务端发送请求,服务端也可以主动的向客户端发送请求;这里是和http协议最大的差别;

    总的来说:http协议服务端响应到客户端是被动的,而webSocket协议服务端请求到客户端是主动的;

    案例说明

    光说概念可能体会不出来,下面我来举个例子:

    看直播时会有实时的弹幕,那么这个弹幕是怎么实时的显示的?

    • 假设使用http协议,客户端请求服务端获取弹幕列表,服务端响应到客户端后确实可以获取到弹幕;但是一次请求就只有一次响应,但是弹幕是实时的,那么这样就无法实时获取到弹幕信息;

      image-20220811231155746

      当然也有解决方法,可以通过轮询,一段时间内就自动发送一次获取弹幕的请求,但是这样的体验也不是特别好,因为只是请求获取的一个时间段的信息;弹幕还好,如果是游戏或者协同编辑等那么体验就非常不好了;

    • 那么使用webSocket协议就可以轻松实现这种操作,只要某个客户端A向服务端发送了弹幕,那么服务端就可以把该弹幕发送给每一个在直播间的客户端,每个客户端就可以接收到该客户端A发送的弹幕了;

      image-20220811232057224


    所以正是因为WebSocket的这种双向通信的特点,它常用于以下领域:

    • 聊天、消息、点赞
    • 直播评论(弹幕)
    • 游戏、协同编辑、基于位置的应用

    代码展示

    下面我就简单展示一下WebSocket的功能,这里就模拟一个弹幕的发送;

    前后端分离项目主要技术:

    后端:java8+springboot+jwt+websocket

    前端:vue3+js+websocket

    因为后端和前端都需要发送webSocket请求,所以前后端都需要配置websocket

    首先介绍以下websocket库中几个重要的方法:

    onOpen() // 连接时调用
    onClose() // 关闭连接时调用
    onMessage() // 获取到信息时调用
    onError() // 出现错误时调用
    send() // 发送webSocket请求(携带数据)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    说一下前后端的交互,既然前后端有信息发送,那么信息的格式就需要确定;http请求时经常用JSON格式进行交互,所以这里前后端最好也使用JSON格式进行交互;

    后端

    后端java代码:

    @Component
    @ServerEndpoint("/websocket/message/{token}")
    @Slf4j
    public class WebSocketMessageServer {
        // onlineUsers可以当成该直播间的所有用户集合,key为用户的id,value为该用户的WebSocketMessageServer对象;
        // 注意:是一个用户有一个WebSocketMessageServer对象!!!!!
        // webSocket是多线程的,所以要使用ConcurrentHashMap保证线程安全
        private static final ConcurrentHashMap<Long, WebSocketMessageServer> onlineUsers = new ConcurrentHashMap<>();
        private User user; // 当前登录的用户(当前登录用户要建立连接)
        private Session session = null; // 该对象可以发送消息给指定用户
    
        private static RedisCacheUtil redisCacheUtil; // redis工具类
    
        // 因为Spring自动注入是单例的,而webSocket是多线程的,所以无法直接注入,可以通过这个方法给每一个线程注入
        @Autowired
        public void setRedisCacheUtil(RedisCacheUtil redisCacheUtil) {
            WebSocketMessageServer.redisCacheUtil = redisCacheUtil;
        }
    
        // 建立连接
        @OnOpen
        public void onOpen(Session session, @PathParam("token") String token) throws IOException {
            this.session = session;
            log.info("connect user...");
            // 获取当前登录的用户
            Long userId = JwtAuthentication.getUserId(token); // 从jwt中获取用户id
            this.user = redisCacheUtil.getCacheObject("login:" + userId); // 从redis中获取用户信息
    		
            if (this.user != null) { // 如果当前登录用户存在,则存入当前直播间集合中
                onlineUsers.put(userId, this); // this是当前登录用户的WebSocketMessageServer对象,可以使用该对象和前端进行信息交互
            } else {
                this.session.close();
            }
            log.info("当前建立连接的用户userId=>" + userId);
        }
    
        // 关闭连接
        @OnClose
        public void onClose() {
            if (this.user != null) {
                log.info("disconnect user..." + this.user.getId());
                onlineUsers.remove(this.user.getId()); // 从该直播间中移除该用户
            }
        }
    
        // 消息通信(前端传来的通信消息)
        @OnMessage
        public void onMessage(String message, Session session) {
            log.info("receive match message...");
            JSONObject data = JSON.parseObject(message); // 接收前端的信息
            String sendUser = data.getString("sendUser"); // 获取弹幕发送者
            String sendMessage = data.getString("message"); // 获取弹幕内容
            // 将该信息发送到每一个连接用户的客户端
            onlineUsers.forEach((key, value) -> {
                JSONObject responseMessage = new JSONObject();
                responseMessage.put("userInfo", sendUser); // 弹幕发送者
                responseMessage.put("message", sendMessage); // 弹幕内容
                // value就是每一个用户的WebSocketMessageServer对象
                value.sendMessage(responseMessage.toJSONString()); // 将弹幕信息发送到前端
            });
        }
    
        /**
         * 发送信息
         * @param message 响应信息
         */
        private void sendMessage(String message) {
            synchronized (this.session) {
                try { // 发送信息
                    this.session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        @OnError
        public void onError(Session session, Throwable error) {
            error.printStackTrace();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    ⚠这个方法一定要理解,主要理解的是:一个用户对应一个WebSocketMessageServer对象;只要有了这个对象,那么这个用户就可以通过该对象给自己的客户端发送信息:

    image-20220812011025750

    大致就是图中的意思,理解了这一点才能在服务端写好交互逻辑,不然你向客户端发送个信息都不知道到底发送给了谁;这一点我认为是最重要的,因为复杂的请求逻辑都是写在后端,只有理解了这里,才能写出逻辑更复杂的功能;


    所以上面我的代码中:

    image-20220812003141790

    这一段就是把从某个客户端A接收到的弹幕发送给所有客户端,这样就实现了所有客户端都会接收到客户端A发送的弹幕;

    前端

    <template>
      <div>
        <ContentField style="text-align: center">
          <ul class="list-group" v-for="message in messages">
            <li class="list-group-item">{{message}}li>
          ul>
        ContentField>
        <div class="col-12" style="text-align: center;">
          <div class="col-sm-10" style="width: 500px; margin-left: 500px; margin-top: 20px;">
            <input class="form-control" id="inputPassword" v-model="inputMessage">
            <button @click="sendMessage" type="button" class="btn btn-warning" style="margin-top: 10px">发送button>
          div>
        div>
      div>
    template>
    
    <script>
    import ContentField from "@/components/ContentField.vue";
    import {onMounted, ref, onUnmounted} from "vue";
    import { useStore } from 'vuex'
    
    
    export default {
      name: "MessageWall",
      components: {
        ContentField
      },
      setup() {
        let messages = ref([]) // 弹幕列表
        let socket = null // socket对象
        const store = useStore()
        let inputMessage = ref('') // 输入的弹幕
    
        onMounted(() => {
          // 创建WebSocket对象,请求地址即为后端的@ServerEndpoint中的地址
          socket = new WebSocket(`ws://127.0.0.1:8080/service/websocket/message/${store.state.user.token}/`)
    
          // 建立连接
          socket.onopen = () => {
            console.log('connected...')
          }
          
           // 接收信息
          socket.onmessage= msg => {
            const data = JSON.parse(msg.data); // 获取信息并解析
            let showMessage = data.userInfo + ':' + data.message
            messages.value.push(showMessage) // 存入弹幕列表
            showMessage = ''
          }
    
          // 结束连接
          socket.onclose = () => {
            console.log("disconnected...");
          }
        })
    
        // 当前页面关闭时(刷新/路由切换)也要关闭连接
        onUnmounted(() => {
          socket.close();
        })
    
        // 发送弹幕(以JSON格式发送到后端)
        const sendMessage = () => {
          console.log(inputMessage.value)
          socket.send(JSON.stringify({
            sendUser: store.state.user.username, // 发送信息的用户名
            message: inputMessage.value // 发送的信息
          }))
        }
    
        return {
          messages,
          sendMessage,
          inputMessage
        }
      }
    }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    前端代码没有什么难的,主要是前端向后端建立连接需要写后端的WebSocket对应的url;

    然后就是正常的建立连接一套流程,接收到后端message就进行处理显示到前端界面;

    而发送弹幕则需要向后端发送请求,携带JSON格式的弹幕数据,后端会通过它的onMessage接收该数据;


    代码就是这样,看一看最后效果吧:

    webSocket

    可以看到我开了三个不同浏览器窗口,每个浏览器窗口模拟一个客户端,三个浏览器都登录了不同的账号:ylx\test\admin三个账号,只要有一个客户端发送弹幕,那么另外两个也就可以接收到,这样就简单实现类一个弹幕发送;


    因为案例比较简单,前端为了保证简洁,就用列表表示一条条弹幕,但是整体逻辑就是这样的,只要理解了这个,就可以做出对应的延伸拓展;

    可以看一下后端日志:

    image-20220812010556380

    开始三个客户端都进行了对应的连接,后面也都接收到了信息;

    再次强调:一定要清楚一个客户端一个WebSocket对象,WebSocket对象可以为该客户端和服务端建立连接;

    总结

    之前了解过websocket,但是没有真正实操过,这两天也是在项目中使用到了websocket,感觉比较有意思,可以通过websocket做很多有趣的功能;所以简单总结复习一下;

    如果有问题欢迎交流!

  • 相关阅读:
    贝锐蒲公英异地组网方案,如何阻断网络安全威胁?
    [GXYCTF2019]BabyUpload
    RC4算法:流密码算法的经典之作
    前端性别判断
    iOS swift5 提示信息显示,提示弹框,第三方框架XHToastSwift
    netty入门前置知识-NIO
    基于SSM的高校疫情管理系统的设计与实现
    【Spring Security 系列】(六)入门案例中各组件的实现类
    PYTHON蓝桥杯——每日一练(简单题)
    力扣每日一题:813. 最大平均值和的分组【0-1背包问题】
  • 原文地址:https://blog.csdn.net/YXXXYX/article/details/126296078