EventSource的原理和使用——结合eladmin实现实时聊天的小例子

1.EventSource的工作原理

EventSource 基于 HTTP 协议实现,通过与服务器建立一个持续连接,实现了服务器向客户端推送事件数据的功能。在客户端,EventSource 对象通过一个 URL 发起与服务器的连接。连接成功后,服务器可以向客户端发送事件数据。在客户端,通过 EventSource 对象注册事件处理函数,以接收来自服务器的事件数据。

以下是 EventSource 的工作原理:

  1. 客户端向服务器发起 HTTP GET 请求,请求一个特定的 URL。

  2. 服务器接收请求,并在 HTTP 头中添加 “Content-Type: text/event-stream”。

  3. 服务器建立一个持续的 HTTP 连接,向客户端发送数据,直到连接被关闭。

  4. 当服务器有新的事件数据要发送时,它将这些数据以特定的格式发送给客户端。事件数据格式如下:

    event: eventName; 
    data: eventData;
    其中,event 字段表示事件名,data 字段表示事件数据。
    
  5. 客户端通过 EventSource 对象注册事件处理函数,以接收来自服务器的事件数据。当客户端接收到来自服务器的事件数据时,它将创建一个 Event 对象,并触发相应的事件处理函数,传递 Event 对象作为参数。Event 对象包含以下属性:

    type:事件类型,通常为 "message"。 
    data:事件数据。 
    lastEventId:上一个事件的 ID。 
    origin:事件源的 URL。
    
  6. 当连接出现错误或被关闭时,客户端将触发 “error” 事件或 “close” 事件,以便进行错误处理或重新连接。总的来说,EventSource 建立了一种持久化的 HTTP 连接,实现了服务器向客户端实时推送事件数据的功能。它非常适合于需要实时更新的应用程序,例如聊天室、股票市场等等。

2.spring boot使用EventSource,实现实时聊天后端服务

首先是一个spring的服务类,eventsource用于处理实时聊天的所有逻辑都在这里面,相关功能如下:

  1. register(String userId):这个方法用于注册一个新的用户到聊天服务。它创建一个新的SseEmitter对象,并将其添加到emitters映射中,这个映射保存了所有在线用户的SseEmitter对象。然后,向所有在线用户发送一条包含所有在线用户列表的消息。
  2. sendMessage(String receiverId, String message):这个方法用于向指定的接收者发送消息。它首先从emitters映射中获取接收者的SseEmitter对象,然后调用其send方法发送消息。
  3. sendAllMessage(String message):这个方法用于向所有在线用户发送消息。它遍历emitters映射中的所有SseEmitter对象,并调用其send方法发送消息。
  4. getOnlineUsers():这个方法用于获取所有在线用户的列表。它遍历emitters映射中的所有键(也就是用户ID),并将它们添加到一个JSON数组中,然后返回这个数组的JSON字符串表示。

代码如下

package me.zhengjie.gen.service;

import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
@Slf4j
public class ChatService {
    private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>(); // emitters用来保存所有连接到服务器的id和SseEmitter对象

    public SseEmitter register(String userId) {
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); // 设置为最大值,几乎等于永不超时
        this.emitters.put(userId, emitter);

        JSONObject result = new JSONObject();
        JSONArray array = new JSONArray();
        result.set("users",array);
        for (String key : emitters.keySet()) {
            JSONObject jsonObject = new JSONObject();
            jsonObject.set("username",key);
            array.add(jsonObject);
        }
        emitter.onCompletion(() -> this.emitters.remove(userId));
        emitter.onTimeout(() -> this.emitters.remove(userId));
        sendAllMessage(JSONUtil.toJsonStr(result));
        return emitter;
    }

    public void sendMessage(String receiverId, String message) {
        SseEmitter emitter = this.emitters.get(receiverId);
        if (emitter != null) {
            try {
                emitter.send(message);
                log.info("发送消息给{},消息为{}", receiverId, message);
            } catch (IOException e) {
                log.error("发送消息时发生错误", e);
            }
        }
    }

    public void sendAllMessage(String message) {
        for (SseEmitter emitter : emitters.values()) {
            try {
                emitter.send(message);
            } catch (IOException e) {
                log.error("发送消息时发生错误", e);
            }
        }
    }

    public String getOnlineUsers() {
        JSONObject result = new JSONObject();
        JSONArray array = new JSONArray();
        result.set("users",array);
        for (String key : emitters.keySet()) {
            JSONObject jsonObject = new JSONObject();
            jsonObject.set("username",key);
            array.add(jsonObject);
        }
        return JSONUtil.toJsonStr(result);
    }
}

接着就是编写一个spring的控制类,用来处理与实时聊天相关的HTTP请求,这个类的主要方法如下:

  1. chat(String userId):当用户打开聊天页面时,前端会发送一个GET请求到"/ssechat/{userId}"。这个方法处理这个请求,调用ChatService的register方法注册用户,并返回一个SseEmitter对象。
  2. sendMessage(String receiverId, String message):当用户在前端发送消息时,前端会发送一个POST请求到"/api/ssechat/{receiverId}"。这个方法处理这个请求,调用ChatService的sendMessage方法向指定的接收者发送消息。
  3. getOnlineUsers():这个方法处理前端发送的GET请求到"/ssechat/onlineUsers",调用ChatService的getOnlineUsers方法获取所有在线用户的列表,并返回这个列表的JSON字符串表示。

代码如下:

package me.zhengjie.gen.rest;

import me.zhengjie.gen.service.ChatService;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@RestController
public class ChatController {
    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping("/ssechat/{userId}")
    public SseEmitter chat(@PathVariable String userId) {
        return this.chatService.register(userId);
    }

    @PostMapping("/api/ssechat/{receiverId}")
    public void sendMessage(@PathVariable String receiverId, @RequestBody String message) {
        this.chatService.sendMessage(receiverId, message);
    }

    @GetMapping("/ssechat/onlineUsers")
    public String getOnlineUsers() {
        return this.chatService.getOnlineUsers();
    }
}

3.前端vue使用EventSource实现实时聊天的相关逻辑

为了简化代码,所以当实时聊天界面打开后就会立即创建一个EventSource对象与服务器进行连接

const sseUrl = 'http://localhost:8000/ssechat/' + username
this.eventSource = new EventSource(sseUrl)

一旦EventSource对象被创建并打开,它就会开始监听服务器发送的消息。代码中,定义了一个onmessage事件处理器来处理从服务器接收到的消息。

this.eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data)
  // 处理数据...
}

当服务器发送一个消息时,这个处理器会被触发。消息的内容可以通过event.data访问,然后可以根据这个数据更新页面状态。 代码还处理了错误情况,如果连接关闭,它会尝试重新连接。

this.eventSource.onerror = function(event) {
  if (this.eventSource.readyState === EventSource.CLOSED) {
    this.eventSource = new EventSource(sseUrl)
  }
}

全部的vue代码如下:

<template>
  <div style="padding: 10px; margin-bottom: 50px">
    <el-row>
      <el-col :span="8">
        <el-card style="width: 100%; min-height: 300px; color: #333">
          <div style="padding-bottom: 10px; border-bottom: 1px solid #ccc">在线用户<span style="font-size: 12px">(点击聊天气泡开始聊天)</span></div>
          <div v-for="user in users" :key="user.username" style="padding: 10px 0">
            <div @click="selectUser(user.username)">
              <span>{{ user.username }}</span>
              <i
                class="el-icon-chat-dot-round"
                style="margin-left: 10px; font-size: 16px; cursor: pointer"
              />
              <span v-if="user.username === chatUser" style="font-size: 12px;color: limegreen; margin-left: 5px">chatting...</span>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="16">
        <div
          style="width: 800px; margin: 0 auto; background-color: white;
                    border-radius: 5px; box-shadow: 0 0 10px #ccc"
        >
          <div style="text-align: center; line-height: 50px;">
            Web聊天室({{ chatUser }})
          </div>
          <div ref="scrollArea" style="height: 350px; overflow:auto; border-top: 1px solid #ccc">
            <ChatBubble
              v-for="(message, index) in messages"
              :key="index"
              :is-remote-user="message.from !== user.username"
              :avatar-name="avatarName"
              :text="message.text"
            />
          </div>
          <div style="height: 200px">
            <textarea
              v-model="text"
              style="height: 160px; width: 100%; padding: 20px; border: none; border-top: 1px solid #ccc;
             border-bottom: 1px solid #ccc; outline: none"
            />
            <div style="text-align: right; padding-right: 10px">
              <el-button type="primary" size="mini" @click="send">发送</el-button>
            </div>
          </div>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import ChatBubble from '../compoments/ChatBubble.vue'
import { mapGetters } from 'vuex'
import { getMessage } from '@/views/model1/api/message'
import { sendMessage } from '@/views/model1/api/sse'
import { getAvatar } from '@/api/system/user'
export default {
  name: 'Sse',
  components: { ChatBubble },
  data() {
    return {
      isCollapse: false,
      users: [],
      chatUser: '',
      avatarName: '',
      text: '',
      messages: [],
      content: ''
    }
  },
  computed: {
    ...mapGetters([
      'user',
      'baseApi'
    ])
  },
  created() {
    this.init()
    console.log(this.user)
  },
  methods: {
    init() {
      const _this = this
      if (this.eventSource) {
        this.eventSource.close()
      }
      // eslint-disable-next-line no-unused-vars
      const username = this.user.username
      const sseUrl = 'http://localhost:8000/ssechat/' + username
      this.eventSource = new EventSource(sseUrl)
      this.eventSource.onmessage = (event) => {
        const data = JSON.parse(event.data) // 对收到的json数据进行解析, 类似这样的: {"users": [{"username": "zhang"},{ "username": "admin"}]}
        console.log(data)
        if (data.users) { // 获取在线人员信息
          _this.users = data.users.filter(user => user.username !== username) // 获取当前连接的所有用户信息,并且排除自身,自己不会出现在自己的聊天列表里
        } else { // 如果服务器端发送过来的json数据 不包含 users 这个key,那么发送过来的就是聊天文本json数据
          // 如果服务器端发送过来的json数据 不包含 users 这个key,那么发送过来的就是聊天文本json数据
          //  // {"from": "zhang", "text": "hello"}
          console.log(222222)
          _this.messages.push(data)
          console.log('1111111111111111111' + JSON.stringify(_this.messages))
        }
      }
      this.eventSource.onerror = function(event) {
        if (this.eventSource.readyState === EventSource.CLOSED) {
          // 如果连接已经关闭,那么尝试重新连接
          this.eventSource = new EventSource(sseUrl)
        }
      }
    },
    getAvatarName(userName) {
      getAvatar(userName).then(res => {
        res ? this.avatarName = res : ''
      })
    },
    selectUser(to) {
      this.chatUser = to
      this.getAvatarName(to)
      getMessage(this.user.username, this.chatUser).then(res => {
        res.sort((a, b) => new Date(a.date) - new Date(b.date))
        this.messages = res
        this.$nextTick(() => {
          const scrollArea = this.$refs.scrollArea
          scrollArea.scrollTop = scrollArea.scrollHeight
        })
      })
    },
    send() {
      if (!this.chatUser) {
        this.$message({ type: 'warning', message: '请选择聊天对象' })
        return
      }
      if (!this.text) {
        this.$message({ type: 'warning', message: '请输入内容' })
      } else {
          // 组装待发送的消息 json
          // {"from": "zhang", "to": "admin", "text": "聊天文本"}
          const message = { from: this.user.username, text: this.text }
          sendMessage(this.chatUser, JSON.stringify(message)) // 将组装好的json发送给服务端,由服务端进行转发
          const msg = { from: this.user.username, text: this.text }
          console.log('msg' + JSON.stringify(msg))
          this.messages.push(msg)
          this.$nextTick(() => {
            const scrollArea = this.$refs.scrollArea
            scrollArea.scrollTop = scrollArea.scrollHeight
          })
          this.text = ''
        }
    }
  }
}
</script>
<style>
.tip {
  color: white;
  text-align: center;
  border-radius: 10px;
  font-family: sans-serif;
  padding: 10px;
  width:auto;
  display:inline-block !important;
  display:inline;
}
.right {
  background-color: deepskyblue;
}
.left {
  background-color: forestgreen;
}
</style>

4.EventSource 与 WebSocket Http 的对比

EventSource 和 WebSocket 都是用于实现客户端与服务器之间实时双向通信的技术,但它们在很多方面有着不同的特点和适用场景。

  1. 协议 EventSource 基于 HTTP 协议,使用的是 HTTP 的长连接机制,而 WebSocket 则是一种独立的协议,与 HTTP 没有关系。

  2. 双向通信 WebSocket 支持双向通信,客户端和服务器都可以主动发送数据。而 EventSource 只支持服务器向客户端的单向通信,客户端只能接收数据,不能主动发送数据。

  3. 数据格式 WebSocket 可以发送任意格式的数据,包括文本、二进制等。而 EventSource 仅支持纯文本格式,采用了一种特殊的格式来传输事件数据。

  4. 浏览器兼容性 WebSocket 是 HTML5 新增的标准,兼容性相对较差,在一些旧版本的浏览器中不支持。而 EventSource 的兼容性相对较好,在大多数现代浏览器中都能够正常工作。

  5. 实时性 WebSocket 的实时性更高,它的通信速度和性能都比 EventSource 更优秀。因为 WebSocket 是双向通信,数据传输的效率更高,而 EventSource 由于是单向通信,数据传输的速度会稍慢一些。

  6. 跨域 WebSocket 和 EventSource 都可以跨域使用,但跨域的设置方式有所不同。

    • WebSocket 需要在服务器端进行配置,允许客户端连接。服务器需要在 HTTP 头中添加 “Access-Control-Allow-Origin” 字段,允许来自指定域名的客户端连接。
    • EventSource 跨域时默认使用 CORS 机制。服务器只需在 HTTP 头中添加 “Access-Control-Allow-Origin” 字段,允许来自指定域名的客户端连接即可。
  7. 浏览器兼容性

    WebSocket 和 EventSource 在浏览器兼容性方面有所不同。

    • WebSocket 在一些较老版本的浏览器中不被支持,例如 IE9 及以下版本。但在现代浏览器中,WebSocket 已经得到了广泛的支持。
    • EventSource 则在较早的浏览器版本中也能够正常工作,包括 IE10 及以上版本、Firefox 6.0 及以上版本、Chrome 13.0 及以上版本等等。但在一些较老的浏览器中,如 Safari 5.1.7 及以下版本,EventSource 可能会遇到一些问题。

总的来说,WebSocket 和 EventSource 都有着自己的优点和适用场景。WebSocket 更适合需要双向通信的应用场景,例如实时游戏、在线协作等等。而 EventSource 则更适合需要单向数据推送的应用场景,例如实时监控、股票行情等等。因此,在选择使用哪种技术时,需要根据具体的需求进行选择。

参考:https://juejin.cn/post/7206261082452721722

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
小程序中使用 WebSocket 实现实时聊天的步骤如下: 1. 在小程序 app.json 文件中添加网络权限: ```json { "permission": { "scope.userLocation": { "desc": "获取用户地理位置信息" }, "scope.record": { "desc": "录音功能,用于发送语音消息" }, "scope.writePhotosAlbum": { "desc": "保存到相册功能,用于保存聊天图片" }, "scope.userInfo": { "desc": "获取用户信息,用于显示聊天用户头像和昵称" }, "scope.websocket": { "desc": "使用 WebSocket,用于实现实时聊天" } } } ``` 2. 在小程序页面中创建 WebSocket 连接: ```javascript // 建立连接 let socketTask = wx.connectSocket({ url: 'wss://your.websocket.url', success() { console.log('WebSocket 连接成功') }, fail() { console.log('WebSocket 连接失败') } }) // 监听 WebSocket 连接状态变化 socketTask.onOpen(() => { console.log('WebSocket 已连接') }) socketTask.onError(() => { console.log('WebSocket 连接错误') }) socketTask.onClose(() => { console.log('WebSocket 已关闭') }) ``` 3. 发送和接收消息: ```javascript // 发送消息 socketTask.send({ data: 'Hello, WebSocket!', success() { console.log('消息发送成功') }, fail() { console.log('消息发送失败') } }) // 接收消息 socketTask.onMessage((res) => { console.log('接收到消息:', res.data) }) ``` 完整的实时聊天代码示例: ```javascript Page({ data: { messages: [] // 聊天消息列表 }, onLoad: function() { // 建立连接 let socketTask = wx.connectSocket({ url: 'wss://your.websocket.url', success() { console.log('WebSocket 连接成功') }, fail() { console.log('WebSocket 连接失败') } }) // 监听 WebSocket 连接状态变化 socketTask.onOpen(() => { console.log('WebSocket 已连接') }) socketTask.onError(() => { console.log('WebSocket 连接错误') }) socketTask.onClose(() => { console.log('WebSocket 已关闭') }) // 接收消息 socketTask.onMessage((res) => { console.log('接收到消息:', res.data) let message = JSON.parse(res.data) let messages = this.data.messages messages.push(message) this.setData({ messages: messages }) }) }, // 发送消息 sendMessage: function(event) { let message = event.detail.value if (message) { let socketTask = wx.connectSocket({ url: 'wss://your.websocket.url' }) socketTask.send({ data: message, success() { console.log('消息发送成功') }, fail() { console.log('消息发送失败') } }) this.setData({ inputMessage: '' }) } } }) ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值