Web端即时通讯技术盘点

788 阅读2分钟

之前有项目涉及Web端即时通讯技术,所以抽空对现有的web即时通讯技术进行了盘点,主流的有:短轮询,Comet(长轮询+基于iframe的http流),SSE,websocket,有挺多文章进行了深入文字介绍,本文直接用画图的形式进行梳理。下面将逐一进行简要介绍,如有不妥之处欢迎大家指出。

一、短轮询

短轮询即传统轮询,使用setTimeout或者setInterval定时请求。

图片1.png 图中紫色虚线代表数据更新(接下来的图片一样)

//前端
getResultByPolling(){
  this.timer = setInterval(this.server.ajax.bind(this,{
    url:'/demo/polling',
    success:(res)=>{
      if(res.data.end)
        clearInterval(this.timer);
    }
  }),200)
}
//服务器端
@RequestMapping(value = "/polling")
public JSONObject polling(HttpServletRequest request) {

    JSONObject object = new JSONObject();
    object.put("end", false);

    int number = (int)(Math.random()*20);
    if(number > 2 && number < 5){
        //模拟有新的数据出现
        object.put("end", true);
    }

    return Result.SUCCESS.toJson(object);
}

短轮询目前依旧是大多数人的选择,包括很多大厂。主要原因,简单,尤其是服务器端,不需要特殊处理。毕竟没有接口也是万万不能的。

二、Comet

Comet它是指一种基于 HTTP长连接的“服务器推送”技术,包含长轮询和基于iframe的http流两种方式。

1.长轮询

长轮询是短轮询的翻版,也可以说是升级版。它又分为两种,一是阻塞式的,另一种是非阻塞式(异步)的。

a,阻塞式

可以理解为由服务器端定时获取数据。

图片5.png

//前端
getResultByLongPollingBlock(){
  this.server.ajax({
    url:'/demo/long-polling/block',
    success:(res)=>{
      console.log('长轮询(阻塞)',res.data);
    }
  })
}
//服务器端
@RequestMapping(value = "/long-polling/block")
public JSONObject longPollingBlock(HttpServletRequest request) {
    log.info("开始请求");
    JSONObject object = new JSONObject();
    object.put("end", false);

    //模拟服务器一直在查找新的数据
    while(true){
        int number = (int)(Math.random()*20);
        if(number > 2 && number < 5){
            //模拟有新的数据出现
            object.put("end", true);
            break;
        }
        else{
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    log.info("请求结束");
    return Result.SUCCESS.toJson(object);
}

b,非阻塞式

服务器端异步处理,数据改变时触发轮询响应,同时关闭连接。

图片2.png

//前端
getResultByLongPolling(namespace){
  this.server.ajax({
    url:`/demo/long-polling/${namespace}`,
    success:(res)=>{
      console.log('长轮询',res)
    }
  })
}

modifyData(namespace){
  this.server.ajax({
    url:`/demo/modify/${namespace}`,
    success:(res)=>{
      console.log('修改数据',res)
    }
  })
}
//服务器端

//长轮询-非阻塞
@RequestMapping(value = "/long-polling/{namespace}")
public DeferredResult<JSONObject> longPolling(HttpServletRequest request,
                                          @PathVariable("namespace") String namespace) {
    log.info("请求开始");
    DeferredResult<JSONObject> deferredResult = new DeferredResult<>(OUT_OF_TIME,Result.OUT_OF_TIME_RESULT.toJson());

    deferredResult.onTimeout(() -> {
        log.info("调用超时");
    });
    deferredResult.onCompletion(() -> {
        log.info("调用完成,移除对namespace:" + namespace + "的监视");
        List<DeferredResult<JSONObject>> list = watchRequests.get(namespace);
        list.remove(deferredResult);
        if (list.isEmpty()) {
            watchRequests.remove(namespace);
        }
    });

    List<DeferredResult<JSONObject>> list = watchRequests.computeIfAbsent(namespace, (k) -> new ArrayList<>());
    list.add(deferredResult);

    log.info("请求结束");
    return deferredResult;
}

//修改数据
@GetMapping(value = "/modify/{namespace}")
public void modifyData(@PathVariable("namespace") String namespace) {

    if (watchRequests.containsKey(namespace)) {
        List<DeferredResult<JSONObject>> deferredResults = watchRequests.get(namespace);
        //通知所有watch这个namespace的变更结果
        for (DeferredResult<JSONObject> deferredResult : deferredResults) {
            deferredResult.setResult(Result.SUCCESS.toJson(namespace + " changed,时间为" + System.currentTimeMillis()));
        }
    }
}

长轮询和短轮询区别: 短轮询是收到请求后服务器立即发送响应,无论数据是否有效。而长轮询是等待有数据可发送时发送响应。优点:明显减少了很多不必要的http请求次数,相比之下节约了资源。缺点:连接挂起也会导致资源的浪费。

2.基于iframe的htmlfile流

图片6.png

//前端
getResultByIframe(){
  let iframe = document.createElement('iframe');
  iframe.src = '/demo/iframe';
  iframe.style = 'display: none';
  document.body.appendChild(iframe);
  //定义的数据处理方法
  window.addMsg = (data)=>{
    console.log(data);
  }
}
//服务器端
@RequestMapping("/iframe")
public void streamIframe(HttpServletRequest request, HttpServletResponse response) throws Exception {
    //模拟服务器一直在查找新的数据
    try {
        for (int i = 0; i < 10; i++) {
            PrintWriter out = response.getWriter();
            out.println("<script>parent.addMsg('hello"+i+"')</script>");
            out.flush();
            log.info("emit:{}", "hello" + i);
            Thread.sleep(1000 * 1);
        }
    } catch (Exception e) {
        e.printStackTrace();
        return ;
    }
}

缺点:

  • 跨域问题。
  • IE、chrome、Firefox会显示加载没有完成,图标会不停旋转。
  • 无法进行错误处理或者跟踪连接的状态。
  • 对于HTTP连接的建立和关闭过程而言,服务器端新产生的数据有可能会因为无法及时发送到客户端而导致客户端的数据丢失。

COMET技术并不是HTML5标准的一部分,不推荐使用。

三、SSE

SSE,是HTML5新增的功能,全称为Server-Sent Events,类似于上述iframe流模式,只不过把iframe获取数据换成了ajax。其响应头的content-type必须为text/event-stream,它标识了响应内容为事件流,客户端若不主动通知关闭连接,会一直等待服务端不断发送响应结果。

图片7.png

//前端
getResultBySSE(){
  if(!window.EventSource){
    console.log('EventSource不被支持');
    return ;
  }
  let evtSource = new EventSource(`/demo/sse`);
  evtSource.onopen = function (event) { // 服务器连接成功
    console.log('成功连接')
  }
  evtSource.onmessage = function(event) {
    console.log('获取到数据', event.data)
    if(event.data == 'hello9') //需求前端主动关闭,否则会循环请求
       evtSource.close();
  }
  evtSource.onerror = function (error) { // 监听错误
    console.log('请求发生错误')
  }
}
//服务器端
@RequestMapping("/sse")
public SseEmitter streamSse() {
    final SseEmitter emitter = new SseEmitter(0L); //timeout设置为0表示不超时
    taskExecutor.execute(() -> {
        try {
            for(int i=0;i<10;i++){
                emitter.send("hello"+i);
                log.info("emit:{}","hello"+i);
                Thread.sleep(1000*1);
            }
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
            return; //避免前端关闭浏览器或者浏览器奔溃导致的异常
        }
    });
    return emitter;
}

兼容性:

WechatIMG546.png IE的兼容性可以通过引入event-source-polyfill解决。

四、Websocket

Websocket,基于HTTP和TCP协议的双向通信技术。客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。

图片8.png

//前端
getResultByWebSocket(){
  if(!window.WebSocket){
    console.log('WebSocket不被支持');
    return ;
  }
  this.websocket = new WebSocket("ws://***/websocket/aaa");
  //连接发生错误
  this.websocket.onerror = function(){
      console.log('websocket:error');
  };
  //连接成功建立
  this.websocket.onopen = function(event){
      console.log('websocket:open');
  }
  //接收到消息
  this.websocket.onmessage = function(event){
      console.log('有新的消息',event.data);
  }
  //连接关闭
  this.websocket.onclose = function(){
    console.log('websocket:close');
  }
  //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
  window.onbeforeunload = this.closeWebSocket.bind(this);
}
//websocket发送消息
postMessage(){
  let message = document.getElementById('message').value;
  if(message) this.websocket.send(message);
}
closeWebSocket(){
  this.websocket.close();
}
//服务器端

//依赖引入
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

//ServerEndpointExporter注入
@Configuration
public class WebSocketConfig {
    private static final long serialVersionUID = 7600559593733357846L;

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

//websocket
@Slf4j
@Component
@ServerEndpoint(value="/websocket/{namespace}")
public class WebsocketController {

    private static String namespace;

    //连接时执行
    @OnOpen
    public void onOpen(@PathParam("namespace") String namespace, Session session) throws IOException{
        this.namespace = namespace;
        log.debug("新连接:{}",namespace);
    }

    //关闭时执行
    @OnClose
    public void onClose(){
        log.debug("连接:{} 关闭",this.namespace);
    }

    //收到消息时执行
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        log.debug("收到用户{}的消息{}",this.namespace,message);
        session.getBasicRemote().sendText(message);
    }

    //连接错误时执行
    @OnError
    public void onError(Session session, Throwable error){
        log.debug("用户id为:{}的连接发送错误",this.namespace);
        error.printStackTrace();
    }
}

兼容性:

WechatIMG547.png

五、SockJS

最后给大家推荐个库-SockJS,SockJS是一个JavaScript库,它为浏览器提供了一个类似WebSocket的对象。它会优先使用原生的WebSocket;如果浏览器不支持,则使用sse;如果sse也不支持,则使用长轮询。 使用SockJS,需要使用对应的服务器端库,目前支持的语言种类也很多。 image.png