服务器推送技术


服务器推送技术

1. 主流服务器推送技术说明

⼀、需求与背景

若⼲年前,所有的请求都是由浏览器端发起,浏览器本身并没有接受请求的能⼒。所以⼀些特殊需求都是⽤ ajax 轮询的⽅式来实现的。⽐如:

  • 股价展示页面实时的获取股价更新
  • 赛事的文字直播,实时更新赛况
  • 通过页面启动一个任务,前媏想知道任务后台的实时运行状态

通常的做法就是需要以较⼩的间隔,频繁的向服务器建⽴ http 连接询问任务状态的更新,然后刷新⻚⾯显示状态。但这样做的后果就是浪费⼤量流量,对服务端造成了⾮常⼤的压⼒。

⼆、服务端推送常⽤技术

在 html5 被⼴泛推⼴之后,我们可以使⽤服务端主动推送数据,浏览器接收数据的⽅式来解决上⾯提到的问题。下⾯我们就为⼤家介绍两种服务端数据推送技术。

全双⼯通信:WebSocket

全双⼯就是双向通信。如果说 http 协议是“对讲机”之间的通话(你⼀句我⼀句,有来有回),那 websocket 就是移动电话(可以随时发送信息与接收信息,就是全双⼯)。

图片描述

本质上是⼀个额外的 tcp 连接,建⽴和关闭时握⼿使⽤ http 协议,其他数据传输不使⽤ http 协议 ,更加复杂⼀些,⽐较适⽤于需要进⾏复杂双向实时数据通讯的场景。在 web ⽹⻚上⾯的客服、聊天室⼀般都是使⽤ WebSocket 协议来开发的。

服务端主动推送:SSE (Server Send Event)

html5 新标准,⽤来从服务端实时推送数据到浏览器端, 直接建⽴在当前 http 连接上,本质上是保持⼀个 http ⻓连接,轻量协议 。客户端发送⼀个请求到服务端 ,服务端保持这个请求连接直到⼀个新的消息准备好,将消息返回⾄客户端。除⾮主动关闭,这个连接会⼀直保持。

  • 建⽴连接
  • 服务端 -> 浏览器(连接保持)
  • 关闭连接

SSE 的⼀⼤特⾊就是重复利⽤ 1 个连接来接收服务端发送的消息(⼜称 event),从⽽避免不断轮询请求建⽴连接,造成服务资源紧张。

三、websocket 与 SSE ⽐较

1 是否基于新协议 是否双向通信 是否支持跨域
SSE 否(Http) 否(单向) 否(Firefox ⽀持跨域)
WebSocket 是(ws)

但是 IE 和 Edge 浏览器不⽀持 SSE,所以 SSE ⽬前的应⽤场景⽐较少。 虽然

websocket 在很多⽐较旧的版本浏览器上⾯也不兼容,但是总体上⽐ SSE 要好不少。另外还有⼀些开源的 JS 前端产品,如 SockJS Socket.IO,在浏览器

端提供了更好的 websocket 前端 js 编程体验,与浏览器有更好的兼容性。

图片描述

2. 服务端推送事件 SSE

⼀、模拟⽹络⽀付场景

⼤家应该都⽤过⽀付系统,⽐如淘宝买⼀个产品之后进⾏扫码⽀付。如果结

合 SSE,该如何实现这个过 程?

  1. ⽤户扫码向⽀付系统(⽀付宝)进⾏⽀付

  2. ⽀付完成之后,告知商户系统(淘宝卖家系统)我已经发起⽀付了(建⽴ SSE 连接)

  3. ⽀付系统(⽀付宝)告诉商户系统(淘宝卖家系统),这个⽤户确实⽀付成功了

  4. 商户系统(淘宝卖家系统)向⽤户发送消息:你已经⽀付成功,跳转到⽀付成功⻚⾯。(通过 SSE 连接,由服务器端告知⽤户客户端浏览器

注意:在返回最终⽀付结果的操作,实现了服务端向客户端的事件推送,以

使⽤ SSE 来实现

⼆、SSE 起步练习

package top.syhan.boot.websocket.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
/**
* @description: SSE练习
* @author: syhan
* @date: 2022-04-18
**/
@Slf4j
@Controller
public class SseController {
    @RequestMapping(value = "/server/info", method = {RequestMethod.GET}, produces = "text/event-stream;charset=UTF-8")
    public ResponseBodyEmitter pushMsg() {
        final SseEmitter emitter = new SseEmitter(0L);
        log.info("emitter push message .....");
        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        try {
            emitter.send(list.toString(), MediaType.TEXT_EVENT_STREAM);
       } catch (IOException e) {
            e.printStackTrace();
       }
        return emitter;
   }
    @RequestMapping(value = "/server/data", method = {RequestMethod.GET}, produces = "text/event-stream;charset=UTF-8")
 public ResponseBodyEmitter push() {
        final SseEmitter emitter = new SseEmitter(0L);
        try {
            Thread.sleep(1000);
       } catch (Exception e) {
            e.printStackTrace();
       }
        double money = Math.random() * 10;
        DecimalFormat df = new DecimalFormat(".00");
        String param = df.format(money);
        try {
            emitter.send("⽩菜价格⾏情:" + param + "元" + "\n\n", MediaType.TEXT_EVENT_STREAM);
       } catch (IOException e) {
            e.printStackTrace();
       }
        return emitter;
   }
}

对于服务器端向浏览器发送的数据,浏览器端需要在 JavaScript 中使⽤ EventSource 对象来进⾏处理。EventSource 使⽤的是标准的事件监听器⽅式,只需要在对象上添加相应的事件处理⽅法即可。EventSource 提供了三个标准事件

事件名称 事件触发说明 事件处理方法
open 当服务器向浏览器第⼀次发送数据 onopen
message 当收到服务器发送的消息时产⽣ onmessage
error 当出现异常时产⽣ onerror

除了使⽤标准的事件处理⽅法,还可以使⽤addEventListener⽅法对事件进⾏监听。

var es = new EventSource('事件源名称') ;  //与事件源建⽴连接
//标准事件处理⽅法,还有onopen、onerror
es.onmessage = function(e) {
};
//可以监听⾃定义的事件名称
es.addEventListener('⾃定义事件名称', function(e) {
});

public/sse-demo1.html

<!DOCTYPE html>
<html lang="en">
  <head>
       
    <meta charset="UTF-8" />
       
    <title>sse-demo1</title>
  </head>
  <body>
    <div id="msg_from_server"></div>
    <script>
      if (!!window.EventSource) {
        let source = new EventSource("http://localhost:8080/server/info");
        let s = "";
        source.addEventListener(
          "open",
          () => {
            console.log("连接打开.");
          },
          false
        );
        source.addEventListener("message", (e) => {
          s += e.data + "<br/>";
          document.getElementById("msg_from_server").innerHTML = s;
        });
        source.addEventListener(
          "error",
          (e) => {
            if (e.readyState === EventSource.CLOSED) {
              console.log("连接关闭");
            } else {
              console.log(e.readyState);
            }
          },
          false
        );
      } else {
        alert(4);
        console.log("没有sse");
      }
    </script>
  </body>
</html>

运⾏效果

运行效果

public/sse-demo2.html

<!DOCTYPE html>
<html lang="en">
  <head>
       
    <meta charset="UTF-8" />
       
    <title>sse-demo2</title>
  </head>
  <body>
    <div id="result"></div>
    <script type="text/javascript">
      //需要判断浏览器⽀不⽀持,可以去w3c进⾏查看
      const source = new EventSource("http://localhost:8080/server/data");
      source.onmessage = function (event) {
        console.info(event.data);
        document.getElementById("result").innerText = event.data;
      };
    </script>
  </body>
</html>

运⾏效果:1 秒后出现服务端推送的数据

运行效果

3. 双向实时通信 websocket

⼀、整合 websocket

  • 添加依赖
<!-- 引⼊websocket依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

编写配置类,开启 websocket 功能

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
   }
}

⼆、兼容 HTTPS 协议

  • WebSocket 的 ws 协议是基于 HTTP 协议实现的
  • WebSocket 的 wss 协议是基于 HTTPS 协议实现的

⼀旦你的项⽬⾥⾯使⽤了 https 协议,websocket 就要使⽤ wss 协议才可以。

在 TomcatCustomizer 配置的基础之上加上如下的代码,就可以⽀持 wss 协议。

@Bean
public TomcatContextCustomizer tomcatContextCustomizer() {
    return new TomcatContextCustomizer() {
        @Override
        public void customize(Context context) {
            context.addServletContainerInitializer(new WsSci(), null);
       }
   };
}

三、WebSocket 编程基础

3.1 连接的建⽴

前端 js 向后端发送 wss 连接建⽴请求

socket = new WebSocket("wss://localhost:8888/ws/asset");

SpringBoot 服务端 WebSocket 服务接收类定义如下:

@Component
@Slf4j
@ServerEndpoint(value = "/ws/asset")
public class WebSocketServer {  
}

3.2 全双工数据交互

前端后端都有

  • onopen 事件监听,处理连接建⽴事件

  • onmessage 事件监听,处理对⽅发过来的消息数据

  • onclose 事件监听,处理连接关闭

  • onerror 事件监听,处理交互过程中的异常

图片描述

3.3 数据发送

浏览器与服务器交换数据

图片描述

前端 JS

socket.send(message);

后端 Java,向某⼀个 javax.websocket.Session ⽤户发送消息。

/**
* 发送消息,每次浏览器刷新,session会发⽣变化。
* @param session session
* @param message 消息
*/  
private static void sendMessage(Session session, String message) throws
IOException{
    session.getBasicRemote().sendText(String.format("%s (From ServerSession ID=%s)",message,session.getId()));
}  
⼀个⽤户向其他⽤户群发

图片描述

服务器向所有在线的 javax.websocket.Session ⽤户发送消息。

/**
* 群发消息
* @param message 消息
*/  
public static void broadCastInfo(String message) throws IOException {
    for (Session session : SessionSet) {  
        if(session.isOpen()){  
            sendMessage(session, message);  
       }  
   }  
}

四、websocket 实现简单聊天

WebSocketServer 核⼼代码

  1. @ServerEndpoint(value = “/ws/asset”)表示 websocket 的接⼝服务地址

  2. @OnOpen 注解的⽅法,为连接建⽴成功时调⽤的⽅法

  3. @OnClose 注解的⽅法,为连接关闭调⽤的⽅法

  4. @OnMessage 注解的⽅法,为收到客户端消息后调⽤的⽅法

  5. @OnError 注解的⽅法,为出现异常时调⽤的⽅法

package top.syhan.boot.websocket.server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author syhan
* @description: webSocket服务层,连接webSocket的时候,路径中传⼀个参数值id,⽤ 来区分不同⻚⾯推送不同的数据
* @date 2022-04-18
*/
@ServerEndpoint(value = "/socket/{id}")
@Component
@Slf4j
public class WebSocketServer {
    /**
     * 静态变量,⽤来记录当前在线连接数,线程安全
     */
    private static int onlineCount = 0;
    /**
     * concurrent包的线程安全Set,⽤来存放每个客户端对应的MyWebSocket对象
     */
    public static ConcurrentHashMap<Integer, WebSocketServer>
webSocketSet = new ConcurrentHashMap<>();
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 传过来的id
     */
    private Integer id = 0;
    /**
     * 连接建⽴成功调⽤的⽅法
     */
      @OnOpen
    public void onOpen(@PathParam(value = "id") Integer param, Session
session) {
        //接收到发送消息的⼈员编号
        this.id = param;
        this.session = session;
        //加⼊set中
        webSocketSet.put(param, this);
        //在线数加1
        addOnlineCount();
        log.info("有新连接加⼊!当前在线⼈数为" + getOnlineCount());
        sendMessage("-连接已建⽴-");
   }
    /**
     * 连接关闭调⽤的⽅法
     */
    @OnClose
    public void onClose() {
        if (id != null && id != 0) {
            //从set中删除
            webSocketSet.remove(id);
            //在线数减1
            subOnlineCount();
            log.info("有⼀连接关闭!当前在线⼈数为" + getOnlineCount());
       }
   }
    /**
     * 收到客户端消息后调⽤的⽅法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("来⾃客户端的消息:" + message);
        this.sendMessage(message);
   }
    /**
     * 发⽣错误时调⽤
     **/
    @OnError
    public void onError(Session session, Throwable error) {
        log.info("发⽣错误");
        error.printStackTrace();
   }
    public void sendMessage(String message) {
        try {
            getSession().getBasicRemote().sendText(message);
       } catch (IOException e) {
            log.info("发⽣错误");
            e.printStackTrace();
       }
   }
    /**
     * 给指定的⼈发送消息
     *
     * @param id     id
     * @param message message
     */
    public void sendToMessageById(Integer id, String message) {
        if (webSocketSet.get(id) != null) {
            webSocketSet.get(id).sendMessage(message);
       } else {
            log.info("webSocketSet中没有此key,不推送消息");
       }
   }
   /**
    * 群发⾃定义消息
    */
    public  void broadcastInfo(String message) {
        for (WebSocketServer item : webSocketSet.values()) {
            item.sendMessage(message);
       }
    }
     public Session getSession() {
        return session;
    }
     public static synchronized int getOnlineCount() {
        return onlineCount;
     }
     public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
     }
     public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
     }
}

客户端代码,public/websocket.html

<!DOCTYPE html>
<html lang="en">
  <head>
       
    <meta charset="UTF-8" />
       
    <title>websocket⻚⾯</title>
  </head>
  <body>
    <div id="app">
         
      <div>
        <label>
              输⼊昵称            
          <input
            type="text"
            v-model="nickname"
            id="nickname"
            placeholder="输⼊昵称"
          />
        </label>
        <button @click="open">连接websocket</button>    
      </div>
         
      <div>
        <label>
             输⼊内容            
          <input
            type="text"
            v-model="content"
            id="content"
            placeholder="输⼊内容"
          />
        </label>
         <button @click="sendMsg">发送消息</button>    
      </div>
         
      <hr />
      <div>
         
        <h3>{{message}}</h3>
      </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data() {
          return {
            ws: null,
            content: "",
            message: "",
            nickname: "",
          };
        },
        methods: {
          open() {
            if (this.nickname === "") {
              alert("昵称不能为空");
              return;
            }
            ws = new WebSocket(
              `ws://localhost:8888/websocket?nickname=${this.nickname}`
            );
            ws.onopen = () => {
              console.log("websocket已经连接");
            };
            ws.onclose = () => {
              console.log("websocket已经关闭");
            };
            ws.onerror = () => {
              console.log("websocket出现异常");
            };
            ws.onmessage = (msg) => {
              console.log(msg);
              this.message = this.message.concat(msg.data);
            };
          },
          sendMsg() {
            ws.send(this.content);
            console.log("发送成功");
            this.content = "";
          },
        },
      });
    </script>
  </body>
</html>

运⾏效果

运行效果

运行效果


文章作者: Syhan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Syhan !
评论
  目录