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请求(携带数据)

说一下前后端的交互,既然前后端有信息发送,那么信息的格式就需要确定;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();
    }
}

⚠这个方法一定要理解,主要理解的是:一个用户对应一个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>

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

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

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


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

webSocket

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


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

可以看一下后端日志:

image-20220812010556380

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

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

总结

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

如果有问题欢迎交流!

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YXXYX

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

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

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

打赏作者

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

抵扣说明:

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

余额充值