SSE: the backend sends a message to the frontend (springboot SseEmitter)

background

There is a project, front-end vue, back-end springboot. Now we need to do a function: when the user is using the system, the administrator publishes an announcement, and the user who uses the system can see the announcement.
Based on this, a simple solution: the front-end uses the JS method setInterval to repeatedly call the back-end announcement acquisition interface. This method has several drawbacks:

  • The time interval of the loop call is not easy to determine: if it is too long, the time limit for obtaining announcements will be delayed; if it is too short, it will put pressure on the server, and many requests will be useless (the announcement release time is uncertain, and it is likely that there will be no new announcements for a few days );
  • Token renewal problem: In the project, the front-end request needs to bring the token, and the token has an expiration time. If the user keeps using it (there is interaction between the front-end and the back-end), the renewal will not be felt. If there is such a scene where the timed cycle interacts with the backend, the token will not expire (the call of the cycle will trigger the renewal). Of course, the request for a certain scene can be excluded during the renewal, but such a design is not Well, because there are too many such scenarios, it will cause difficulties in maintenance.

So I thought, if the backend actively pushes messages to the frontend, this problem can be perfectly solved.

plan

There are two solutions to push messages from the backend to the frontend:

  1. use websockets;
  2. use sse;

Here is the way to introduce SSE (if the system has strict requirements on the accuracy and reliability of such messages, use websocket, the use of websocket is relatively complicated); if you want to know the detailed basic knowledge of SSE, you can refer to
Ruan Yifeng This article by the teacher: Server-Sent Events Tutorial

SSE backend code

In SpringMVC, this function has been integrated, so there is no need to introduce additional jar packages, just upload the code directly:

@RestController
@RequestMapping("/notice")
public class NoticeController {
    
    

    @Autowired
    private NoticeService noticeService;

    @GetMapping(path = "createSseEmitter")
    public SseEmitter createSseEmitter(String id) {
    
    
        return noticeService.createSseEmitter(id);
    }

    @PostMapping(path = "sendMsg")
    public boolean sendMsg(String id, String content) {
    
    
        noticeService.sendMsg(id, content);
        return true;
    }

}

@Slf4j
@Service
public class NoticeServiceImpl implements NoticeService {
    
    
    @Autowired
    @Qualifier("sseEmitterCacheService")
    private CacheService<SseEmitter> sseEmitterCacheService;

    @Override
    public SseEmitter createSseEmitter(String clientId) {
    
    
        if (StringUtil.isBlank(clientId)) {
    
    
            clientId = UUID.randomUUID().toString().replace("-", "");
        }
        SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId);
        log.info("获取SSE,id={}", clientId);
        final String id = clientId;
        sseEmitter.onCompletion(() -> {
    
    
            log.info("SSE已完成,关闭连接 id={}", id);
            sseEmitterCacheService.deleteCache(id);
        });
        return sseEmitter;
    }
    @Override
    public void sendMsg(String clientId, String content) {
    
    
        if (sseEmitterCacheService.hasCache(clientId)) {
    
    
            SseEmitter sseEmitter = sseEmitterCacheService.getCache(clientId);
            try {
    
    
                sseEmitter.send(content);
            } catch (IOException e) {
    
    
                log.error("发送消息失败:{}", e.getMessage(), e);
                throw new BusinessRuntimeExcepption(CustomExcetionConstant.IO_ERR, "发送消息失败", e);
            }
        } else {
    
    
            log.error("SSE对象不存在");
            throw new BusinessRuntimeExcepption("SSE对象不存在");
        }
    }
}

Here, only the core code is listed. In short, two things need to be done:

  1. The front end first initiates a request to create a SseEmitter, that is, the createSseEmitter method, which must return a SseEmitter object;
  2. The returned SseEmitter must be cached in the backend (I use ehcache, or you can directly define a map to cache);

Why do you want to do this? Look at the following, and analyze the back-end code together to understand.

front-end code

Because I need to bring a token when I request this interface, so I can't use EventSource directly, and this IE doesn't support it either. So I chose a tool: event-source-polyfill.

  1. Install event-source-polyfill first
npm install event-source-polyfill
  1. Then use:
import {
    
     EventSourcePolyfill } from "event-source-polyfill";
  created() {
    
    
    let _this = this;
    this.source = new EventSourcePolyfill(
      "/" +
        process.env.VUE_APP_MANAGER_PRE_API_URL +
        "/notice/createSseEmitter?id=" +
        uuid(),
      {
    
    
        headers: {
    
    
          [process.env.VUE_APP_OAUTH_AUTHORIZATION]: store.getters.getToken,
        },
        //重连时间间隔,单位:毫秒,默认45000毫秒,这里设置为10分钟
        heartbeatTimeout: 10 * 60 * 1000,
      }
    );

    this.source.onopen = () => {
    
    
      console.log("NOTICE建立连接");
    };
    this.source.onmessage = (e) => {
    
    
      _this.scrollMessage = e.data;
      console.log("NOTICE接收到消息");
    };
    this.source.onerror = (e) => {
    
    
      if (e.readyState == EventSource.CLOSED) {
    
    
        console.log("NOTICE连接关闭");
      } else if (this.source.readyState == EventSource.CONNECTING) {
    
    
        console.log("NOTICE正在重连");
        //重新设置header
        this.source.headers = {
    
    
          [process.env.VUE_APP_OAUTH_AUTHORIZATION]: store.getters.getToken,
        };
      } else {
    
    
        console.log(e);
      }
    };
  },

A few notes:

  • In the new EventSourcePolyfill, you can bring in the header
  • heartbeatTimeout is a heartbeat time, by default, after heartbeatTimeout interval, it will trigger to reconnect the backend interface;
  • this.source.headers, the function of this line is to reset the header when reconnecting. If not, then when reconnecting, the parameter information used is still the same as the initial one (including the id in the url in this example ). And because in my project, if other token operations trigger a token refresh, the effective token may change, so here is the token placed in the cache instead of the original token.
    Well, this basically achieves the functions we need.

pay attention

The front-end is configured with a proxy, so the message sent by the back-end has not been received, try to add the following parameters:

  devServer: {
    
    
    compress:false,
    …………
}

question

When writing the backend, I mentioned two questions: Why should I return the SseEmitter object? Why cache SseEmitter objects?
In fact, after reading the principle of SSE, you should understand: this is a long connection, the front end calls the interface to create the SseEmitter object, although the interface returns, but it is not over (this is why the SseEmitter object is returned, if it returns another object , which is no different from the normal interface, the interface ends directly), please see the screenshot below:
insert image description here
After the request is initiated, it has been pending and has not ended. After 10 minutes, the request is canceled (the reconnection of the front-end settings ), and then re-initiate the connection, and the re-initiated connection is also waiting. Only after receiving the message, the status code of this request is 200, but at this time the connection has been established. The details are not described here.
Therefore, if you use the SseEmitter object to send a message, the front end can receive the message of the object (that is, the back end sends a message to the front end). The SseEmitter object used here is the object returned by the createSseEmitter interface (that is, which SseEmitter object is used can send a message to which front end). This is why the SseEmitter object is cached.

Effect

By calling the send message interface, the front end can immediately display the sent message:
insert image description here

Guess you like

Origin blog.csdn.net/fyk844645164/article/details/126680347