Web实时通信技术

本周在应用宝前端分享会上分享了Web实时通信技术,分享内容整理如下。

一、传统Web数据更新

传统的Web数据更新,必须要刷新网页才能显示更新的内容。这是浏览器采用的是B/S架构,而B/S架构是基于HTTP协议的。HTTP协议的工作模式就是客户端向服务器发送一个请求,服务器收到请求后返回响应。所以这种工作模式是基于请求显示数据的。

这样的工作方式有其自身的好处,但是也会导致很多问题。在Web应用越来越火的今天,经常会遇到需要服务器主动发送数据到客户端的需求,比如事件推送、Web聊天等。这些需求使用传统的Web数据更新工作模式是无法实现的,因此就需要一项新的技术:Web实时通信技术。

二、短轮询

第一种解决方法思路很简单,既然需要客户端发送请求服务器才能发送数据,那么就可以让客户端不断的向服务器发送数据,这样就能实时的获取服务器端的数据更新了。具体的实现方法很简单,客户端每隔一定时间就发送一个请求到服务器端。下面的图可以清晰的反映出短轮询过程中客户端和服务器的工作流程:

下面看一下实现方法。

在服务器端我们模拟数据的发送,生成1-1000的随机数,当数值小于800的时候模拟没有数据的情况,大于800的时候模拟有数据的情况,并返回数据:

<?php
$arr = array(‘title‘=>‘推送!‘,‘text‘=>‘推送消息内容‘);
$rand = rand(1,999);
if(rand < 800){
echo “”
}else{
  echo json_encode($arr);
}
?>

客户端部分,定义了一个函数用来发送ajax请求到客户端,然后每隔2s就发送以此请求:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>短轮询ajax实现</title>
<script type="text/javascript" src="../jquery.min.js"></script>
</head>
<body>
<form id="form1" runat="server">
     <div id="news"></div>
    </form>
</body>
<script type="text/javascript">
  function showUnreadNews()
    {
        $(document).ready(function() {
            $.ajax({
                type: "GET",
                url: "setInterval.php",
                dataType: "json",
                success: function(msg) {
                    $.each(msg, function(id, title) {
                        $("#news").append("<a>" + title + "</a><br>");
                    });
                }
            });
        });
    }
    setInterval(‘showUnreadNews()‘,2000);
</script>
</html>

运行程序我们可以在Chrome的network工具看到,每隔两秒都会有一个请求从客户端发往服务器,不管当时的服务器有没有数据,都会立即返回请求。、

短轮询虽然简单,但是它的缺点也是显而易见的。首先短轮询建立了很多HTTP请求,而且其中绝大部分的请求是没有用处的。而HTTP连接数过多过多会严重影响Web性能。其次,客户端设置的请求发送时间间隔也不好掌控,时间间隔太短会造成大量HTTP的浪费,而时间间隔过长会使得客户端不能即时收到服务器端的数据更新,失去了即时通信的意义。

三、长轮询

针对上面短轮询的种种问题,我们自然而然想到要减少HTTP请求的数量,才能让实时通信性能更高。而长轮询就能有效的减少HTTP请求的数量。

长轮询的逻辑是,首先客户端向服务器端发送一个请求,服务器端在收到请求后不马上返回该请求,而是将请求挂起。一段时间后服务器端有数据更新时,再将这个请求返回客户端,客户端收到服务器端的响应数据后渲染界面,同时马上再发送一个请求到服务器,如此循环,下面的图描述了这个过程:

长轮询有效的减少了HTTP连接。服务器端在有数据更新时才返回数据,客户端收到数据再请求这一机制,较少了之间许多的无用HTTP请求。下面通过一个Demo来演示长轮询的工作模式。

服务器端模拟数据更新,在客户端发来请求后先挂起6s,模拟6s后才有数据的情况:

<?php
$arr = array(‘title‘=>‘推送‘,‘text‘=>‘推送消息内容‘);
$flag = 0;
for($i=1;$i<=6;$i++){
  if($i>5){   //i = 6时表示有数据了
    echo json_encode($arr);
  }else{
    sleep(1);
  }
}
?>

这里为了演示的更清楚,添加了一个for循环,其实就是先将请求挂起6s。

客户端发送一个ajax请求,并当收到服务器端数据后自动再发送一个请求到服务器:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>长轮询ajax实现</title>
<script type="text/javascript" src="../jquery.min.js"></script>
</head>
<body>
<input type="button" id="btn" value="click">
<div id="msg"></div>
</body>
<script type="text/javascript">
$(function(){
        $("#btn").bind(‘click‘,{btn:$(‘#btn‘)},function(e){
            $.ajax({
                type: ‘POST‘,
                dataType: ‘json‘,
                url: ‘do.php‘,
                timeout: ‘20000‘,
                success: function(data,status){
                    $("#msg").append(data.title + ‘:‘ + data.text + "</br>");
                    e.data.btn.click();
                }
            });
        });
    });
</script>
</html>

长轮询虽然有效的减少了HTTP请求,但是HTTP请求相比之下还是很多的,因为每次数据的更新都需要建立一个HTTP请求。下面的技术就可以实现建立以此HTTP连接,服务器可以源源不断的向客户端发送数据。

四、SSE

MessageEvent是HTML5中新定义的一种事件基类,和原先的MouseEvent、UIEvent一样。MessageEvent是专门为数据传输定义的事件,HTML5中的SSE和WebSocket都利用了这个事件。MessageEvent在HTML5协议中的接口如下:

除了继承了Event事件具有的属性外,MessageEvent还定义了其他属性。其中data属性所包含的内容就是传输的数据内容。而lastEventId可以存放一个事件标识符,当客户端和服务器传输数据过程中断开连接需要重连时,客户端会将上一次传输数据的lastEventId作为请求头中的一个特殊字段发送到服务器,从而让服务器可以继续上次断开连接的部分发送消息。

SSE是HTML5规范中定义的,它可以实现维持一个HTTP连接,服务器端单向向客户端不断发送数据。这个技术真正意义上实现了服务器主动推送数据到客户端。除此之外,SSE的客户端和服务器端代码都特别简洁,使用起来十分方便。

SSE的逻辑就是首先客户端发送请求,建立一个HTTP连接,之后服务器端和客户端一直保持这个连接,服务器端可以单向向客户端发送数据,见下图:

SSE的实现方法很简单,SSE是以事件流的形式发送数据的。在服务器端要先进行如下配置:

Content-Type:text/event-stream
Cache-Control:no-cache
Connections:keep-alive

如果还需要进行跨域,配置里再添加:

Access-Control-Allow-Origin: *

其中text/event-stream是HTML5规范中为SSE的事件流传输定义的一种流格式。

做好配置后,服务器第二个要做的事就是维护一个清单,清单内容就是要向客户端发送的数据,下面是一段例子:

data: first event  

event: push
data: second event

每一组事件流传输对应的数据,每个事件流之间使用换行符进行分割。这种格式发送过去之后会被进行解析,最后将各个部分进行组装,客户端按需进行读取。每一个事件流可以为其指定四个字段。

(1)retry字段

SSE有一个自动重连机制,即客户端和服务器之间的连接断开后,隔一段时间客户端就会自动重连。retry指定的就是这个重连时间,单位为ms。

(2)event字段

SSE中有三个默认的事件,它们都继承自MessageEvent事件类。其中open事件在连接建立时触发,message事件在从客户端接收到数据时触发,close事件在连接关闭时触发。如果传输事件时一个事件流没有设定event字段的值,那么客户端就会监听message默认事件;如果指定了event事件,那么就会生成自定义的event事件,客户端可以监听自定义的event进行数据读取。

(3)data字段

data字段包含的内容就是服务器要传送给客户端的数据,SSE只能传送文本数据,且必须是UTF-8编码的。由于这些事件都继承自MessageEvent基类,因此可以通过event.data获取服务器传输的数据。

(4)id字段

id字段是事件的唯一标识符,解析后会被传入MessageEvent对应的lastEventId属性字段中,从而记录上次数据传输的位置。如果不指定id字段lastEventId字段就是一个空字符串。

至此服务器端任务完成,下面介绍客户端的实现方法。

客户端首先需要实例化一个EventSource对象。EventSource对象在HTML5中的接口定义如下:

首先需要为EventSource对象传入一个url,表明要请求的服务器地址。该对象有三个readyState状态值,其中CONNECTING表示正在建立连接,OPEN表示连接处于打开状态可以传输数据,CLOSED状态表示连接中断,并且客户端并没有尝试重连。EventSource定义的默认事件句柄为onopen、onmessage、onerror。其中的方法只有close(),用来关闭连接。

实例化好EventSource对象后,我们需要对事件进行监听,从而获取数据,最后可以通过close()方法关闭连接,整体逻辑的代码如下:

var es = new EventSource(url);
es.addEventListener("message", function(e){
    console.log(e.data);
})
es.close();

下面是一个实现SSE的例子。

服务器使用node,代码及注释如下:

var http = require("http");
var fs = require("fs");
//创建服务器
http.createServer(function (req, res) {
  var index = "./index.html";
  var fileName;
  var interval;
  var i = 1;
  //设置路由
  if (req.url === "/"){
    fileName = index;
  }else{
    fileName = "." + req.url;
  }
  if (fileName === "./stream") {
    //配置头部信息:注意类型为专门为sse定义的event-stream,并且不使用缓存
    res.writeHead(200, {"Content-Type":"text/event-stream", "Cache-Control":"no-cache", "Connection":"keep-alive"});
    /*
      下面的代码的输出结果等价于:
      retry: 10000
      event: title
      data: News Begin

      data: ...

      ...
    */
    //上面可以看出,只有第一段是触发事件connecttime,其他都是触发默认事件message
    res.write("retry: 10000\n");    //定义连接断开后客户端重新发起连接的时间,ms制
    res.write("event: title\n");   //自定义的事件title
    res.write("data: News Begin! \n\n");
    //每隔1s就在协议中新写入一段数据来模拟服务器向客户端发送数据
    interval = setInterval(function() {
      res.write("data: News" + i +"\n\n");
      i++;
    }, 1000);
    //监听close事件,当服务器关闭时停止向客户端传送数据
    req.connection.addListener("close", function () {
      clearInterval(interval);
    }, false);
  } else if (fileName === index) {
    fs.exists(fileName, function(exists) {
      if (exists) {
        fs.readFile(fileName, function(error, content) {
          if (error) {
            res.writeHead(500);
            res.end();
          } else {
            res.writeHead(200, {"Content-Type":"text/html"});
            res.end(content, "utf-8");
          }
        });
      } else {
        res.writeHead(404);
        res.end();
      }
    });
  } else {
    res.writeHead(404);
    res.end();
  }
}).listen(8888);
console.log("Server running at http://127.0.0.1:8888/");

服务器端自定义了title事件,用来发送标题数据,其他的数据使用默认事件发送。

客户端部分代码及注释如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Server-Sent Events Demo</title>
  <meta charset="UTF-8" />
  <script>
    window.onload = function() {
      var button = document.getElementById("connect");
      var status = document.getElementById("status");
      var output = document.getElementById("output");
      var connectTime = document.getElementById("connecttime");
      var source;
      function connect() {
        source = new EventSource("stream");
        //messsage事件:当收到服务器传来的数据时触发
        source.addEventListener("message", function(event) {
          output.textContent = event.data;  //每次收到数据后都更新时间
        }, false);
        //自定义的事件title
        source.addEventListener("title", function(event) {
          connectTime.textContent = event.data;
        }, false);
        //open事件:当客户端和服务器完成连接时触发
        source.addEventListener("open", function(event) {
          //每次连接成功后更新按钮功能和文本提示,再次点击按钮应为关闭连接
          button.value = "Disconnect";
          button.onclick = function(event) {
            //调用eventsource对象的close()方法关闭连接,并且为其绑定新的事件connect建立连接
            source.close();
            button.value = "Connect";
            button.onclick = connect;
          };
        }, false);
        //异常处理
      }
      //调用,如果支持EVentSource则执行connect()方法仅从sse连接
      connect();
    }
  </script>
</head>
<body>
  <input type="button" id="connect" value="Connect" /><br />
  <span id="status"></span><br />
  <span id="connecttime"></span><br />
  <span id="output"></span>
</body>
</html>

客户端监听事件,不同的事件收到的数据进行不同的渲染。同时,都过为按钮绑定事件,调用close()等方法,实现SSE连接的打开与断开。

SSE技术简单方便,且是HTML5中定义内容,实现了服务器推送数据的技术,下图是SSE的浏览器兼容性列表:

但是SSE只能实现服务器到客户端单向的数据传输。有时我们的需求需要使用双向数据传输,这时就需要使用WebSocket。

五、WebSocket

WebSocket也是HTML5中定义的。它是一个新的协议,实现了全双工的通信模式,即客户端和服务器端可以互相发送消息。WebSocket的实现首先需要客户端和服务器端进行一次握手,此后就会建立起一个数据传输通道,通道存在期间客户端和服务器端可以平等的互相发送数据。具体的逻辑图如下:

WebSocket的服务器端实现比较复杂,但是各个后台语言都已经有实现好的WebSocket库。比如Node.js中的nodejs-websocket模块和socket.io模块。使用WebSocket技术可以实现很多功能,附件中就是借助nodejs-websocket模块编写的弹幕效果。

WebSocket的客户端实现比较便捷,首先需要实例化一个WebSocket对象,传入要请求的服务器的url。这里需要注意,协议名要指定为ws或wss,如:

ws = new WebSocket("ws://localhost:8080");

客户端可以通过调用send()方法进行数据的发送,通过调用close()方法关闭WebSocket连接。WebSocket也使用了MessageEvent接口,因此可以对消息事件进行监听,默认的可以通过监听message事件获取数据。下面是WebSocket在HTML5规范中定义的接口:

WebSocket的服务器端实现可以分为两个部分,第一个部分是握手部分,主要负责HTTP协议的升级,第二个部分是数据传输部分。

WebSocket协议可以说是一个HTTP协议的升级版,这个升级过程需要通过客户端和服务器的一次握手来实现。下面是建立握手时客户端向服务器发送的请求报文头实例:

字段Upgrade:websocket和Connection:Upgrade部分完成了协议的升级,服务器可以触发响应事件,获取这两个字段的内容,匹配符合要求后服务器端进行握手处理。客户端需要向服务器端发送一个Sec-WebSocket-Key字段,这个字段的内容是客户端产生的,相当于一个私钥。服务器端收到客户端的请求头后,如果确定是要使用WebSocket协议,就开始进行握手。服务器端使用客户端传来的Sec-WebSocket-Key的值,与服务器端存储的一个全局唯一标识符进行拼接,之后做SHA1处理和BASE64加密,并作为响应头返回给客户端。服务器端的GUID相当于公钥。

下面是服务器端返回的响应报文头,处理后的字符串在Sec-WebSocket-Accept字段中给出。客户端必须受到101状态码,这个状态码表示切换协议,从而完成对协议的升级。

总的来看,WebSocket只有在建立握手连接的时候借用了HTTP协议的头,连接成功后的通信部分都是基于TCP的连接,可以说WebSocket协议是HTTP协议的升级版。

WebSocket的数据帧格式如下:

opcode存储的是传输数据的类型,诸如文本、二进制数据等。数据传输时首先会对该部分的值进行判断,然后进行对应的数据操作。数据存储在Payload Data字段中。最后将帧结构解析为一个键值对的对象。

下面是浏览器对WebSocket的支持情况:

时间: 2024-10-28 16:03:27

Web实时通信技术的相关文章

Comet技术详解:基于HTTP长连接的Web端实时通信技术

前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Server-sent Events). 关于这4种技术方式的优缺点,请参考<Web端即时通讯技术盘点:短轮询.Comet.Websocket.SSE>.本文将专门讲解Comet技术.(本文同步发布于:http://www.52im.net/thread-334-1-1.html) 学习交流 - 即时通

Web实时通讯技术简介

一.概述 1.Web端即时通讯技术 即时通讯技术简单的说就是实现这样一种功能:服务器端可以即时地将数据的更新或变化反应到客户端,例如消息即时推送等功能都是通过这种技术实现的.但是在Web中,由于浏览器的限制,实现即时通讯需要借助一些方法.这种限制出现的主要原因是,一般的Web通信都是浏览器先发送请求到服务器,服务器再进行响应完成数据的现实更新. 2.实现Web端即时通讯的方法 实现即时通讯主要有四种方式,它们分别是短轮询.长轮询(comet).长连接(SSE).WebSocket.它们大体可以分

ASP.NET Core环境Web Audio API+SingalR+微软语音服务实现web实时语音识别

处于项目需要,我研究了一下web端的语音识别实现.目前市场上语音服务已经非常成熟了,国内的科大讯飞或是国外的微软在这块都可以提供足够优质的服务,对于我们工程应用来说只需要花钱调用接口就行了,难点在于整体web应用的开发.最开始我实现了一个web端录好音然后上传服务端进行语音识别的简单demo,但是这种结构太过简单,对浏览器的负担太重,而且响应慢,交互差:后来经过调研,发现微软的语音服务接口是支持流输入的连续识别的,因此开发重点就在于实现前后端的流式传输.参考这位国外大牛写的博文Continuou

rsync、inotify实现web实时同步

. rsync.inotify实现web实时同步,布布扣,bubuko.com

Python项目实战教程:web实时聊天室项目

新课强力来袭:基于Node.js的web实时聊天室项目! 麦子学院新课以马踏飞燕般的速度生粗来啦(*^__^*) .小伙伴们你萌确定不来一发吗? 啦啦啦--上图镇楼↓↓↓ 那些神奇的传送门→_→ 本课程:http://www.maiziedu.com/course/others/597-8698/ 李大大主页:http://www.maiziedu.com/group/common/course/59404/ 这里是正儿八经的课程介绍(快看快看o( ̄ヘ ̄o#)): 网站实时通讯一体化解决方案,采

Python web实时消息服务器后台推送技术方案---GoEasy

Goeasy, 它是一款第三方推送服务平台,使用它的API可以轻松搞定实时推送!个人感觉goeasy推送更稳定,推送速度快,代码简单易懂上手快浏览器兼容性:GoEasy推送支持websocket 和polling两种连接方式,从而可以支持IE6及其以上的所有版本,同时还支持其它浏览器诸如Firefox, Chrome, Safari 等等.支 持不同的开发语言:   GoEasy推送提供了Restful API接口,无论你的后台程序用的是哪种语言都可以通过RestfulAPI来实现后台实时推送.

C(++) web实时消息服务器后台推送技术方案---GoEasy

Goeasy, 它是一款第三方推送服务平台,使用它的API可以轻松搞定实时推送!个人感觉goeasy推送更稳定,推送速度快,代码简单易懂上手快浏览器兼容性:GoEasy推送支持websocket 和polling两种连接方式,从而可以支持IE6及其以上的所有版本,同时还支持其它浏览器诸如Firefox, Chrome, Safari 等等.支 持不同的开发语言:   GoEasy推送提供了Restful API接口,无论你的后台程序用的是哪种语言都可以通过RestfulAPI来实现后台实时推送.

node.js web实时消息服务器后台推送技术方案---GoEasy

Goeasy, 它是一款第三方推送服务平台,使用它的API可以轻松搞定实时推送!个人感觉goeasy推送更稳定,推送速度快,代码简单易懂上手快浏览器兼容性:GoEasy推送支持websocket 和polling两种连接方式,从而可以支持IE6及其以上的所有版本,同时还支持其它浏览器诸如Firefox, Chrome, Safari 等等.支 持不同的开发语言:   GoEasy推送提供了Restful API接口,无论你的后台程序用的是哪种语言都可以通过RestfulAPI来实现后台实时推送.

Websocket web实时消息服务器后台推送技术方案---GoEasy

Goeasy, 它是一款第三方推送服务平台,使用它的API可以轻松搞定实时推送!个人感觉goeasy推送更稳定,推送速度快,代码简单易懂上手快浏览器兼容性:GoEasy推送支持websocket 和polling两种连接方式,从而可以支持IE6及其以上的所有版本,同时还支持其它浏览器诸如Firefox, Chrome, Safari 等等.支 持不同的开发语言:   GoEasy推送提供了Restful API接口,无论你的后台程序用的是哪种语言都可以通过RestfulAPI来实现后台实时推送.