您的位置 首页 php

第70节 Server Push服务端推送Comet、SSE和WebSocket-JavaScript

数据交互有两种模式:Pull(拉模式)和Push(推模式);

Pull拉模式:
指的是客户端主动向服务端发出请求,获取相关数据;
优点:此过程由客户端发起请求,所以不存在数据积压的问题;
缺点:可能不够及时,对客户端来说需要考虑数据获取的相关逻辑,例如,何时去获取、获取的频率如何控制等;

Push推模式:
指的是客户端与服务端建立长连接,服务端如果有新数据,直接通过长连接通道推送到客户端,其是RealTime Web(实时化Web)应用的基础;
优点是:响应迅速及时、不需要刷新,一旦有数据更新,客户端立马能感知到,因此会节省网络流量,大大地提高了用户体验;对客户端来说逻辑简单,不需要关心何时去获取数据以及获取的频率是多少。
缺点:服务端不知道客户端的数据消费能力,可能导致数据积压在客户端,来不及处理。

服务端推送常用的技术有Commet、 SSE 和WebSocket。

Comet:
Comet是Alex Russell发明的一个词,指的是一种更高级的Ajax技术,被称为“服务器推送”;Ajax是一种从客户端向服务器请求数据的技术,即Pull拉模式,而Comet则是一种服务器向客户端推送数据的技术,即Push推模式,因此Comet能够让数据按流实时地被推送到客户端上。
有两种实现Comet的方式:长轮询和流;

轮询 Polling
也称为传统轮询或短轮询,其原理就是客户端以一定的时间间隔,频繁地向服务端发出请求,以保持客户端和服务端的数据同步;
(短)轮询的时间线,如图:

轮询

例如,后端 polling .php:

 echo "Web前端开发";  

前端:polling.html:

 var xhr = new  xmlhttp Request();
xhr.onready state change = function(){
if(xhr.readyState == 4 && xhr.status == 200){
console.log(xhr.responseText);
}
};
setInterval(function(){
xhr.open("GET", "polling.php");
xhr.send(null);
},3000);  

这个技术最大的问题就是客户端发出请求和服务器端的更新并不是一致的;客户端以固定的频率向服务器发出请求,可能服务器端并没有更新,此时返回的是个空的信息;
更新的时候,有可能客户端并没有请求;
当服务端响应的数据量过大,此时,客户端就会触发超时;
这样多次的请求不仅浪费了资源,而且并不是实际上的实时更新;

示例,polling_insert.html:

 <h1>插入记录</h1>
<form>
用户名:<input type="text" name="username" id="username"><br/>
性别:<input type="radio" name="sex" id="male" value="1" checked>男
<input type="radio" name="sex" id=" female " value="0">女<br/>
年龄:<input type=" number " name="age" id="age"><br/>
<input type="button" value="插入" id="btnInsert">
</form>
<script>
var btnInsert = document.getElementById("btnInsert");
btnInsert.addEventListener("click",  function (){
var xhr = new XMLHttpRequest();
xhr.open("POST","polling_data.php");
xhr.onload = function(event){
if(xhr.response.status){
 alert ("添加成功");
}else{
alert("添加失败");
}
};
var formData = new FormData(document.forms[0]);
formData.append("action", "insert");
xhr.responseType = " JSON ";
xhr.send(formData);
});
</script>  

表users:

 CREATE TABLE `users` (
`ID` int(11) NOT NULL,
`username` varchar(100) NOT NULL,
`sex` int(1) NOT NULL DEFAULT '1',
`age` int(3) NOT NULL DEFAULT '0',
`flag` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;  

polling_data.php:

 <?php
require_once("conn.php");
if($_REQUEST['action']){
$action = $_REQUEST['action'];
// 插入数据
if($action == 'insert'){
$username = $_REQUEST['username'];
$sex = $_REQUEST['sex'];
$age = $_REQUEST['age'];
$sql = "insert into users(username,sex,age,flag) values('$username','$sex','$age',0)";
$result = $conn->query($sql);
if($result){
echo '{"status": 1}';
}else{
echo '{"status": 0}';
}
}elseif($action == 'getdata'){
$sql = "select * from users where flag=0 order by ID ASC limit 1";
$result = $conn->query($sql);
$row = mysqli_fetch_array($result);
if($row){
$sql = "update users set flag=1 where ID=".$row['ID'];
if($conn->query($sql)){
echo json_encode($row);
}
}
}
}  

polling_getdata.html:

 <script>
window.onload = function(){
var result = document.getElementById("result");
 if (!result){
result = document.createElement("ul");
result.id = "result";
document.body.appendChild(result);
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
if(xhr.response){
var data = xhr.response;
var li = document.createElement("li");
li.innerHTML = "姓名:" + data.username +
"性别:" + (data.sex ? "男" : "女") +
"年龄:" + data.age +
"时间:" + (new Date).getTime();
result. append Child(li);
}
}
};
xhr.responseType = "json";
setInterval(function(){
xhr.open("GET", "polling_data.php?action=getdata");
xhr.send(null);
},3000);
}
</script>  

长轮询:
长轮询是传统轮询的一个翻版,其把短轮询颠倒了一下,即客户端向服务端发起一个请求后,就被挂起,然后服务端一直保持连接打开,看有没有更新的数据,如果有更新的数据,服务端主动把数据推送到客户端;如果没有更新数据,那就一直保持连接的状态(挂起),而客户端也不做多余的请求;客户端收到数据后,随即又发起一个到服务端的新请求;这一过程在页面打开期间一直持续不断;
长轮询的时间线:

长轮询

通过这种机制就可以减少无效的客户端和服务端之间的交互,但是也会浪费资源;

长轮询实现步骤:
1、发起Polling:
向服务端发起请求,此时服务端还未应答,所以客户端与服务端之间一直处于连接状态;

2、数据推送:
如果服务器端有相关数据,会将此数据通过此前建立的通道(连接)发回客户端;

3、Polling终止:
Polling终止情况有三种:

  • 若服务端返回相关数据,此时客户端收到数据后,关闭请求连接,结束此次Polling过程;
  • 若客户端等待超时后,服务端仍然没有返回数据,此时客户端需要主动终止此次Polling请求;
  • 若客户端遇到网络故障或异常,此时客户端也需要主动终止此次Polling请求;

4、重新Polling:
终止上次Polling后,客户端需要立即再次发起Polling请求,这样才能保证及时的拉取数据;

例如,polling_long.html:

 <div id="msg"></div>
<input type="button" id="btn" value="longPolling" />
<input type="button" id="btnStop" value="stopPolling" />
<script>
var btn = document.getElementById("btn");
btn.addEventListener("click", longPolling);
var xhr = null;
function longPolling(){
var msg = document.getElementById("msg");
xhr = new XMLHttpRequest();
xhr.open("GET", "polling_long.php?time=80");
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
var data = xhr.response;
if(data){
// 从服务器得到数据,显示数据并继续查询
if(data.success == "1"){
msg.innerHTML += "<br/>[有数据] " + data.text;
longPolling();
}
// 未从服务器得到数据,继续查询
if(data.success == "0"){
msg.innerHTML += "<br/>[无数据]" + data.text;
longPolling();
}
}
}
};
xhr.onerror = xhr.timeout = function(){
msg.innerHTML += "<br/>[超时]";
longPolling();
};
xhr.onabort = function(){
msg.innerHTML += "<br/>[停止]";
}
xhr.responseType = "json";
xhr.timeout = 800000;
xhr.send(null);
}
var btnStop = document.getElementById("btnStop");
btnStop.addEventListener("click", function(){
if(xhr)
xhr.abort();
});
</script>  

后端polling_long.php:

 <?php
if(!isset($_GET['time'])){
exit();
}
set_time_limit(0);
$i = 0;
while (true) {
// sleep(1);
 usleep (500000);
$i++;
$ rand  = rand(1, 999);
if($rand <= 100){
$arr = array("success"=>"1", "text"=>"随机数:$rand");
echo json_encode($arr);
exit();
}
if($i == $_GET['time']){
$arr = array("success"=>"0","text"=>"无数据:$rand");
echo json_encode($arr);
exit();
}
}  

无论是短轮询还是长轮询,浏览器都要在接收数据之前,先发起对服务器的连接;两者最大的区别在于服务端如何发送数据:短轮询是服务端立即发送数据,无论数据是否有效,而长轮询是等待新数据,有则发送响应,否则保持连接;
长轮询同样也存在缺点,由于把请求挂起,所以会导致服务端资源的浪费;
轮询的优势是所有浏览器都支持,因为使用XHR对象和计时器就能实现。

示例,聊天室:

polling_chat.html:

 <input type='text' style='width:70px' placeholder="昵称" id='name'/>
<input type='text' placeholder="Input Your Message" id='input'/> <span id='error'>用户名和信息不能为空</span>
<input type="button" id="btnSend" value="发送" />
<pre></pre>
<script>
window.onload = function(){
var spanError = document.getElementById("error");
spanError.style.display = "none";
var preMessage = document. getElementsByTagName ("pre")[0];
var btnSend = document.getElementById("btnSend");
btnSend.addEventListener("click", function(event){
var target = event.target;
target.setAttribute("disabled", true);

var name = document.getElementById("name").value;
var input = document.getElementById("input").value;
if(input == "" || name == ""){
spanError.style.display = "block";
target.removeAttribute("disabled");
}else{
time = Date.now();
var param = "method=send&time=" + time + "&input=" + input + "&name=" + name;
var url = "polling_chat.php?" + param;
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = function(){
if(xhr.status == 200){
if(xhr.readyState == 4){
document.getElementById("input").value = "";
}
}else{
preMessage.innerHTML += time + ' | 发送失败 |' + param + '<br/>';
}
target.removeAttribute("disabled");
}
xhr.ontimeout = function(){
preMessage.innerHTML += time + ' | 发送超时 | <br/>';
}
xhr.timeout = 5000;
xhr.send(null);
}
});

time = ""; // 初始化时间
var xmlhttp;
function getData(time){
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", "polling_chat.php?method=get&time=" + time);
xmlhttp.onreadystatechange = function(){
if(xmlhttp.status == 200){
if(xmlhttp.readyState == 4){
var data = xmlhttp.response;
if(data){
time = Date.now(); // 如果有新信息则更新时间
for(var i=0, len=data.length; i<len; i++){
preMessage.innerHTML += data[i]['time'] + ' | ' + data[i]['name'] + ' -> ' + data[i]['input'] + '<br/>';
}
}
getData(time);
}
}else{
getData(time);
}
}
xmlhttp.onerror = xmlhttp.timeout = function(){
getData(time);
};
xmlhttp.timeout = 10000; // 这个可设长一点
xmlhttp.responseType = "json";
xmlhttp.send(null);
}
getData(time);
};
</script>  

polling_chat.php:

 <?php
set_time_limit(0); //设置超时限制
 define ('MAX',20); //限制最大缓存消息个数
define('SLEEP',50000); //休息时间/用于缓解服务器压力 单位: 微秒  //usleep(1000000)一秒
$type = $_GET['method'];
$time = $_GET['time'];
$input = $_GET['input'];
$name = $_GET['name'];
$data File  = "polling_data.json";
switch ($type) {
case 'send':
//前端发信息后端存入JSON文件
$f = new MyFile();
$arr = $f->read($dataFile);
if(empty($arr)){
$arr = array();
}
// 获取原来的信息
$num = count($arr);
if($num >= MAX){
//设置最大保存信息个数,防止JSON文件过大
unset($arr[0]); //删除第一个(最老的)
$arr[$num] = array(
'time' => $time,
'name' => $name,
'input' => $input
);
$arr = array_values($arr);//重新建立 索引 
}else{
$arr[$num] = array(
'time' => $time,
'name' => $name,
'input' => $input
);
}
$data = json_encode($arr);
$f->write($dataFile, $data);
echo '200';
break;
case 'get':
while(true){
$f = new MyFile();
$arr = $f->read($dataFile);
$j = 0;
$newArr = array();
if(empty($arr)){
$arr = array(); //初始话
}
for($i = 0; $i < count($arr); $i++){
// 比较time,判断是否为新消息
if((double)$arr[$i]['time'] >= (double)$time){
$newArr[$j] = $arr[$i];
$timex =  substr ($arr[$i]['time'],0,-3); //时间戳转换为时间
$newArr[$j]['time'] = date("m/d H:i:s",$timex);
$j++;
}
}
// 查询新消息,有新消息,则输出,并跳出while循环
if(!empty($newArr[0])){
echo json_encode($newArr);
break;
}
usleep(SLEEP);
}
break;
}
class MyFile {
function write($filepath,$content){
file_put_contents($filepath, $content);
}
function read($filepath){
if(file_exists($filepath)){
$str = file_get_contents($filepath);
return json_decode($str,true);
}else{
self::write($filepath,'');
}
}
}
polling_data.json:
[{"time":"1595939294719","name":"wangwei","input":"\u5927\u5e08\u54e5"}]  

HTTP流:
第二种流行的Comet的实现是使用HTTP流;流不同于轮询,因为它在页面的整个生命周期内只使用一个HTTP连接;具体来说,就是浏览器向服务端只发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据;
例如streaming.php,使用PHP实现的服务端的流形式:

 <?php
set_time_limit(0);
$i = 0;
$rand = 1;
while(true){
// echo "Number is ".$i++.", rand:".$rand;
// ob_flush();
// flush();
echo str_pad("Number is ".$i++.", rand is ".$rand."<br/>", 4096*32);
$rand = rand(1,10);
sleep($rand);
}  

在标准浏览器中,通过侦听readystatechange事件及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流;
如:streaming.html:

 <input type="button" id="btnStart" value="streaming" />
<script>
var btnStart= document.getElementById("btnStart");
btnStart.addEventListener("click", function(){
var xhr = new XMLHttpRequest();
xhr.open("GET","streaming.php",true);
xhr.onreadystatechange = function(){
console.log("readyState:" + xhr.readyState);
console.log("接收:" + xhr.responseText);
};
xhr.send(null);
});
</script>  

当readyState值变为3时,responseText属性中已经保存接收到的所有数据;
如修改streaming.html:

 // ...
if(xhr.readyState == 3){
console.log("readyState:" + xhr.readyState);
console.log("接收:" + xhr.responseText);
}  

此时,就需要比较此前接收到的数据,决定从什么位置开始取得最新的数据;例如修改streaming.html:

 <input type="button" id="btnStart" value="streaming" />
<input type="button" id="btnStop" value="stop" />
<script>
// 包装一个函数,用于请求,并截取最新数据
var xhr = null;
function createStreamingClient(url, progress, finished){
xhr = new XMLHttpRequest();
var received = 0;
xhr.open("GET", url, true);
xhr.onreadystatechange = function(){
var result;
if (xhr.readyState == 3) {
// 只取得最新数据并调整计数器
result = xhr.responseText.substring(received);
// 前后有可能会有空格,去除
result = result.trim();
received += result.length;
// 调用progress回调函数
// result有可能为空字符,需要过滤
if(result){
progress(result);
}
}else if(xhr.readyState == 4){
finished(xhr.responseText);
}
}
xhr.send(null);
return xhr;
}
var btnStart = document.getElementById("btnStart");
btnStart.addEventListener("click", function(){
var client = createStreamingClient("streaming.php",
function(data){
console.log("接收:" + data);
},
function(data){
console.log("结束");
});
});
var btnStop = document.getElementById("btnStop");
btnStop.addEventListener("click", function(){
if(xhr)
xhr.abort();
});
</script>  

服务器推送事件SSE:
SSE(Server-Sent Event,服务器推送事件)是围绕Comet交互推出的API或者模式;SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据;
服务器响应的MIME类型必须是text/event-stream,而且是浏览器中的JavaScript API能够解析的格式;SSE支持短轮询、长轮询和HTTP流,而且能在断开连接时自动确定何时重新连接;
除了IE,标准浏览器都支持SSE。

SSE API:
SSE API与其他传递消息的API很相似,要预订新的事件流,首先要创建一个新的EventSource对象,并传进一个入口点:

 var source = new EventSource("myevents.php");
console.log(source);  

EventSource对象默认不能跨域,如果要跨域,需要遵守CORS;

服务端返回的MIME类型必须为text/event-stream,否则抛出异常,如myevents.php:

 <?php
header("Content-Type: text/event-stream");
while(true){
echo "data: zeronetwork, time is ". date('r');
echo str_pad('',4096);
echo "\n\n";
ob_flush();
flush();
sleep(rand(1, 5));
}  

EventSource属性和方法:

  • readyState属性:值为CONNECTION:0表示正连接到服务器、值为OPEN:1表示打开了连接、值为CLOSED:2表示关闭了连接;
  • url属性:返回事件源的URL;
  • withCredentials属性:一个布尔值,指示EventSource对象是否使用跨源(CORS)凭据;
  • close()方法:关闭链接;
 // ...
console.log(source.readyState); // 0
console.log(source.url); // 
console.log(source.withCredentials); // false
source.close();
console.log(source.readyState); // 2  

EventSource事件:

  • onopen:在建立连接时触发;
  • onmessage:在从服务器接收到新事件数据时触发;
  • onerror:在无法建立连接时触发;
 function handler(event){
console.log(event);
}
source.onopen = handler;
source.onmessage = handler;
source.onerror = handler;  

open和error事件类型为Event;
message事件类型为MessageEvent,如:

 source.onmessage = function(event){
console.log(event);
console.log(event.data);
};  

MessageEvent接口:
代表一段被目标对象接收的消息事件;
属性:

  • data属性:其保存着服务器(推送)发回的字符串数据;
  • lastEventId:表示事件的唯一ID;
  • origin:返回一个表示消息发送者来源;
  • ports:MessagePort对象数组,表示消息正通过特定通道(数据通道)发送的相关端口;
  • source:是一个MessageEventSource对象,代表消息发送者;
 source.onmessage = function(event){
console.log(event.data); // zeronetwork,time...
console.log(event.lastEventId); // 空
console.log(event.origin); // 
console.log(event.ports); // []
console.log(event.source); // null
};  

通常来说,只需要关注data属性;

默认情况下,EventSource对象会保持与服务器的活动连接;如果连接断开,还会重新连接;这就意味着SSE适合轮询、长轮询和HTTP流;

如果要强制立即断开连接并且不再重新连接,可以调用close()方法,如:

 source.close();
// 或
setTimeout(function(){
source.close();
console.log(source.readyState); // 2
},5000);  

前后端整理:

 <input type="button" id="btnOpen" value="SSE连接">
<input type="button" id="btnClose" value="关闭SSE">
<script>
var source = null;
var btn = document.getElementById("btnOpen");
btn.addEventListener("click", function(){
source = new EventSource("myevents.php");
source.onopen = function(event){
console.log("SSE已连接,readyState:" + source.readyState);
};
source.onmessage = function(event){
console.log(event.data);
};
source.onerror = function(event){
console.log("出错,正连接,readyState:" + source.readyState);
};
});
var btnClose = document.getElementById("btnClose");
btnClose.addEventListener("click", function(){
source.close();
console.log("SSE已关闭,readyState:" + source.readyState);
});
</script>  

后端myevents.php:

 $rand = 1;
while(true){
echo "data: zeronetwork, rand: ".$rand.", time: ". date('r');
echo str_pad('',4096);
echo "\n\n";
ob_flush();
flush();
$rand = rand(1,10);
sleep($rand);
}  

服务端响应:
服务端事件数据会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/event-stream,响应的数据必须符合一定的格式;

其格式为四种特殊的消息类型:
1.data:具体的消息体,可以是对象或字符串,也就是EventSource对象的data属性;
具体格式为:”data: “+ message +”\n\n”;即每个数据项都带有前缀data:,data: 后面有一个空格,并以两个换行符\n\n结尾,表示当前消息体发送完毕,如果是一个\n,表示当前消息并未结束,浏览器需要等待后面数据的到来再触发事,形式如:

 data: zeronetwork\n\n
data: wangwei\n\n
data: zeronetwork\n
data: wangwei\n\n  

后端实现:

 echo "data: zeronetwok\n\n";
echo "data: wangwei"; // 也可以分别写在两行上
echo "\n\n"; // 如果没有\n\n,该data会被忽略
echo "data: Web\n"; // 与下面一个data为同一个data
echo "data: 前端开发\n\n";
echo "data: 大师哥"; // 如果没有,则与下面的字符串为同一个data
echo "王唯\n\n";  

2.event:指定自定义消息事件的名称,如:event: customEvent;
在后端发送data类型的消息,前端onmessage事件会自动获取;但EventSource规范还允许服务端指定自定义事件,即使用event类型指定自定义消息事件的名称,然后客户端就可以侦听该事件;如,后端php的while中添加:

 echo "event: myevent\n";
echo "data: 自定义的事件类型myevent\n\n";  

event后只用一个\n,后面一定要有data,代表通过该事件的data获取的数据;

此时前端可以侦听自定义事件:

 source.addEventListener("myevent",function(event){
console.log(event); // MessageEvent
console.log(event.data); // 自定义的事件...
});  

例如,后端修改添加:

 $i=0;
while(true){
if(($i++) % 5 == 0){
echo "event: systemevent\n";
echo "data: 系统消息\n\n";
}
if($i % 7 == 0){
echo "event: imageevent\n";
echo "data: images/1.jpg\n\n";
}  

前端:

 source.addEventListener("systemevent", function(event){
console.log(event.data);
});
source.addEventListener("imageevent", function(event){
console.log(event.data);
});  

3. id:
为当前事件消息的标识符,可以不设置;如果设置了,使用EventSource对象的lastEventId就可以获取;
该id行可以位于data或event行前面或后面,但应该位于前面,因为它是为位于它下方的data或event设置id的,形如:

 id: 1\n\n
data: zeronetwrok\n\n  

例如:后端php修改为:

 $i=0;
while(true){
echo "id: myid".($i++)."\n\n";
echo "data: zeronetwork, time is ". date('r'). "\n\n";
echo str_pad('',4096);
ob_flush();
flush();
sleep(rand(3, 5));
}  

在前端的onmessage中,使用:

 console.log(event.lastEventId + ":" + event.data); // myid0:zeronetwork...  

即可获取该id;
设置了id后,EventSource对象会跟踪上一次触发的事件消息;如果连接断开,会向服务器发送一个包含名为Last-Event-ID的特殊HTTP头部请求,以便服务端知道下一次该触发哪个事件;
在多次连接的事件流中,这种机制可以确保浏览器以正确的顺序接收到连接的数据段;

4.retry:
设置当前http连接失败后,重新连接的间隔;EventSource规范规定,客户端在http连接失败后默认进行重新连接,重连间隔为3s,通过设置retry字段可指定重连间隔;如:后端添加:

 echo "retry: 1000\n";  

5.注释:
当出现一个没有名称的字段而只有“:”时,就会被服务端理解为“注释”,并不会被发送至浏览器端;

由于EventSource是基于HTTP连接之上的,因此在一段没有响应数据的时期会出现超时问题;服务端默认HTTP超时时间为2分钟,此时可以在服务端设置超时默认值;但规范中规定了注释行可以用来防止连接超时,服务器可以定期发送一条消息注释行,以保持连接不断;如:

 while(true){
echo ": 注释的内容\n\n";
ob_flush();
flush();
sleep(119);
}  

IE不支持EventSource,可以为其提供一下兼容方案:

 // 模拟EventSource
if (window.EventSource === undefined) {
window.EventSource = function(url){
var xhr;
var evtsrc = this;
var charsReceived = 0;
var type = null;
var data = "";
var eventName = "message";
var lastEventId = "";
var retrydelay = 1000;
var timer;
var aborted = false;
xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
// console.log(xhr.readyState + ":" + xhr.status);
switch(xhr.readyState){
case 3:
processData();
break;
case 4:
reconnect();
break;
}
};
connect();
function reconnect(){
if(aborted) return;
if(xhr.status >= 300) return;
timer = setTimeout(connect, retrydelay);
}
function connect(){
charsReceived = 0;
type = null;
xhr.open("GET", url);
xhr.setRequestHeader("Cache-Control", "no-cache");
if(lastEventId)
xhr.setRequestHeader("Last-Event-ID", lastEventId);
xhr.send(null);
}
this.close = function(){
xhr.abort();
if(timer)
clearTimeout(timer);
aborted = true;
}
function processData(){
if(!type){
type = xhr.getResponseHeader("Content-Type");
if(!/^text\/event-stream/.test(type)){
aborted = true;
xhr.abort();
return;
}
}
var chunk = xhr.responseText.substring(charsReceived);
charsReceived = xhr.responseText.length;
var lines = chunk.replace(/(\r\n|\r|\n)$/, "").split(/\r\n|\r|\n/);
for(var i=0; i<lines.length; i++){
var line = lines[i],
pos = line.indexOf(":"),
name,
value = "";
if(pos == 0) continue;
if(pos > 0){
name = line.substring(0, pos);
value = line.substring(pos+1);
if(value.charAt(0) == " ")
value = value.substring(1);
}else
name = line;
switch(name){
case "event":
eventName = value;
break;
case "data":
data += value + "\n";
break;
case "id":
lastEventId = value;
break;
case "retry":
retrydelay = parseInt(value) || 1000;
break;
default:
break; // 忽略其他行
}
if(line == ""){
if(evtsrc.onmessage && data !== ""){
if(data.charAt(data.length-1) == "\n")
data = data.substring(0, data.length-1);
data = data.replace(/\n/,"");

evtsrc.onmessage({
type: eventName,
data: data,
origin: url,
lastEventId: lastEventId
});
}
data = "";
continue;
}
}
}
}
}  

可以使用第三方的库,如:eventsource.min.js 地址:
,只要引入该库,IE所有版本均完美支持。

示例:简易聊天室,sse_chat.html:

 <p><input type="text" id="input" style="width:50%" placeholder="请输入..." /></p>
<p><input type="button" id="btnOpen" value="SSE连接">
<input type="button" id="btnClose" value="关闭SSE"></p>
<script>
window.onload = function(){
var chat = null;
var nick;
var btnOpen = document.getElementById("btnOpen");
btnOpen.addEventListener("click", function(){
nick = prompt("输入用户昵称");
// nick = "wangwei";
var input = document.getElementById("input");
input.focus();
// 载入旧数据
var xhr = new XMLHttpRequest();
xhr.open("GET", "sse_chat.php?action=getold");
xhr.onload = function(event){
var datas = JSON.parse(xhr.response);
// console.log(datas);
for(var i=0,len=datas.length; i<len; i++){
var data = datas[i];
var node = document.createTextNode(data.username + ":" + data.content);
var div = document.createElement("div");
div.appendChild(node);
document.body.insertBefore(div, input.parentNode);
input.scrollIntoView({behavior:"smooth"});
}
xhr = null;
};
xhr.send(null);

chat = new EventSource("sse_chat.php?action=getdata");
chat.onmessage = function(event){
var data = event.data;
data = JSON.parse(data);
console.log(data);
var node = document.createTextNode(data.username + ":" + data.content);
var div = document.createElement("div");
div.appendChild(node);
document.body.insertBefore(div, input.parentNode);
input.scrollIntoView({behavior:"smooth"});
};
chat.onopen = chat.onerror = function(event){
// console.log(event);
};
console.log("SSE正在连接,readyState:" + chat.readyState);
btnOpen.setAttribute("disabled",true);
btnClose.removeAttribute("disabled");
});
// 使用Ajax把用户的消息发送给服务器
input.onchange = function(){
var param = "action=send&username=" + nick + "&content=" + input.value;
var xhr = new XMLHttpRequest();
xhr.open("POST", "sse_chat.php",true);
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
xhr.send(param);
input.value = "";
};
var btnClose = document.getElementById("btnClose");
btnClose.setAttribute("disabled", true);
btnClose.addEventListener("click", function(){
chat.close();
console.log("SSE已关闭,readyState:" + chat.readyState);
btnClose.setAttribute("disabled", true);
btnOpen.removeAttribute("disabled");
});
}
</script>  

数据表chat:

 CREATE TABLE `chat` (
`id` int(11) NOT NULL,
`username` varchar(100) NOT NULL,
`content` varchar(255) NOT NULL,
`datetime` int(11) DEFAULT '0'
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;  

sse_chat.html:

 <?php
header("Content-Type: text/event-stream");
require_once("conn.php");
if(isset($_REQUEST['action'])){
if($_REQUEST['action'] == 'getold'){
$sql = "select * from chat";
$query = $conn->query($sql);
$result = array();
while($row = $query->fetch_array()){
$result[] = $row;
}
echo json_encode($result);
}elseif($_REQUEST['action'] == 'getdata'){
$time = time() - 5; // 取5s内的数据
$sql = "select * from chat where datetime > $time";
$result = $conn->query($sql);
// 判断查询结果是否为空
if($result->num_rows){
while($row = $result->fetch_array()){
echo "data: ".json_encode($row)." \n\n";
}
sleep(2);
}
}elseif($_REQUEST["action"] == 'send'){
$username = $_POST['username'];
$content = $_POST['content'];
$time = time();
$sql = "insert into chat(username, content, datetime) values('$username', '$content', '$time')";
$result = $conn->query($sql);
if($result){
echo '{"status": 1}';
}else{
echo '{"status": 0}';
}
}
}  

Web Sockets:

Web Sockets不是一门技术,是一种网络通信协议,也称为套接字,可以在一个单独的持久连接上提供全双工传输通信。
Ajax(XHR)和Comet(SSE)都是通过HTTP通信的,而HTTP通信的特点是:是一种无状态的协议,其由客户端请求和服务端响应组成,而且只能由客户端发出请求。
而WebSocket最大的特点就是,客户端和服务端只需要完成一次握手,两者之间就可以创建持久性的连接,之后服务端就可以主动向客户端推送消息,客户端也可以主动向服务器发送信息,是双向的通信。

WebSocket工作原理:

在网络分层上,WebSocket与http协议都是应用层的协议,它们都是基于tcp传输层的,但在使用Web Socket建立连接时,会先使用一个HTTP的101 switch protocol请求来发送到服务器以发起连接,进行协议转换(Upgrade),在取得服务器响应后,建立的连接会从HTTP协议切换为WebSocket协议,这也称为协议转换,这个动作也称为握手,而握手的过程只需要一次,就可以实现持久的连接。

Web Socket是基于TCP的独立协议,与HTTP协议有着非常好的兼容性(它的默认端口也是80和443),但其使用了自定义的协议,所以两者的URL模式不同,未加密的连接不再是http://,而是ws://;加密的连接也不是https://,而是wss://;在使用Web Socket URL时,必须使用这个模式。
Web Socket在客户端和服务器之间发送的数据量少、性能和通信效率高。
Web Socket没有同源限制,因此可以与任意服务器通信,至于是否会与某个域中的页面通信,则完全取决于服务端。
即可以发送文本,也可以发送二进制数据。
Web Socket是继Ajax、各类Comet技术之后,服务器推送技术的新趋势。

Web Socket API:
要创建Web Socket,先实例化一个WebSocket对象并传入要连接的URL,如:

 var socket = new WebSocket("ws://localhost:8080");
console.log(socket); // WebSocket  

注意,必须给WebSocket构造函数传入绝对URL;

实例化WebSocket对象后,浏览器就会马上尝试创建连接,此时有可能会触发一系列事件;
WebSocket事件:WebSocket的4个事件,在连接生命周期的不同阶段触发:

  • onopen:在成功建立连接时触发;
  • onmessage:当服务器向客户端发来消息时;
  • onerror:在发生错误时触发,连接不能持续;
  • onclose:在连接关闭时触发;
 socket.onopen = function(event){
console.log("Connection open.");
console.log(event); // Event
};
socket.onmessage = function(event){
console.log(event); // MessageEvent
};
socket.onerror = function(){
console.log("Connection error.");
console.log(event); // Event
};
socket.onclose = function(){
console.log("Connection closed.");
console.log(event); // CloseEvent
};  

在这4个事件中:
open和error事件是Event类型;
close事件是CloseEvent类型,其evant对象有额外的信息;

  • wasClean属性:是一个布尔值,表示连接是否已经明确地关闭;
  • code:服务端返回的数据状态码;
  • reason:是一个字符串,包含服务端发回的消息,可以把这些信息显示给用户,也可以记录到日志中以便将来分析;
 socket.onclose = function(){
console.log("Was clean? " + event.wasClean + " Code=" + event.code + " Reason=" + event.reason);
};  

onmessage事件:
当服务器向客户端发来消息时,WebSocket对象就会触发message事件;该事件属于MessageEvent类型,与其他传递消息的API一样,也是把返回的数据保存在event.data属性中,如:

 socket.onmessage = function(event){
var data = event.data;
// 处理数据
}  

与XHR和SSE类似,WebSocket也有一个表示当前状态的readyState属性,不过,这个属性的值与XHR与SSE并不相同,而是如下所示:

  • WebSocket.OPENING (0):正在建立连接;
  • WebSocket.OPEN (1):已经建立连接;
  • WebSocket.CLOSING (2):正在关闭连接;
  • WebSocket.CLOSE (3):已经关闭连接;

WebSocket没有readystatechange事件,但readyState的状态值却几乎能与它的4个事件相对应,其中,readyState的值永远从0开始;如:

 console.log(socket.readyState); // 0
socket.onopen = function(event){
console.log("Connection open. readyState:" + socket.readyState); // 1
};  

onclose事件中,readyState为3;

发送和接收数据:
Web Socket打开之后,就可以发送和接收数据;要向服务器发送数据,使用send()方法并传入任意字符串,如:

 socket.send("大师哥王唯");  

应该在open事件处理函数中执行该方法,如:

 socket.onopen = function(event){
console.log("Connection open.");
socket.send("大师哥王唯");
};  

服务器响应的数据可能是文本,也有可能是二进制数据(blob对象或Arraybuffer对象),因此,在使用响应的数据前,应该判断它的数据类型,如:

 socket.onmessage = function(event){
if(typeof event.data == "string"){
console.log("接收的字符串是:" + event.data);
}else if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("接收的是ArrayBuffer");
}
};  

binaryType属性:返回WebSocket连接所传输二进制数据的类型,返回值为“blob”或”arraybuffer”;如:

 // ArrayBuffer
socket.binaryType = "arraybuffer";
socket.onmessage = function(event){
console.log(socket.binaryType);
console.log(event.data);
console.log(event.data.byteLength);
};
// Blob
socket.binaryType = "blob";
socket.onmessage = function(event){
console.log(socket.binaryType);
console.log(event.data);
console.log(event.data.size);
};  

Web Sockets可以发送纯文本数据,也可以发送二进制,如:

 socket.addEventListener("open",function(event){
// 发送文本
socket.send("大师哥王唯");
// 发送Blob对象
var file = document.querySelector('input[type="file"]').files[0];
socket.send(file);
// 发送ArrayBuffer对象
var binary = new Uint8Array(200);
socket.send(binary);
});  

bufferedAmount属性:
是一个只读属性,用于返回已经被send()方法放入队列中但还没有被发送到网络中的数据的字节数;一旦队列中的所有数据被发送至网络,则该属性值将被重置为0;但是,若在发送过程中连接被关闭,则属性值不会重置为0;如果不断地调用send(),则该属性值会持续增;可以用它来判断发送是否结束,如:

 socket.onopen = function(event){
console.log("Connection open.");
var data = new ArrayBuffer(1000000);
socket.send(data);
if (socket.bufferedAmount == 0) {
console.log("发送完毕");
}else{
console.log("发送还没有结束:" + socket.bufferedAmount); // 1000000
}
};  

extensions是只读属性,返回服务端已选择的扩展值,目前扩展值只有空字符串或者一个扩展列表;

 console.log(socket.extensions); // ""  

protocol 是个只读属性,用于返回服务器端选中的子协议的名字;

 console.log(socket.protocol); // ""  

在创建WebSocket 对象时,可以在参数protocols中指定子协议的字符串;

 var socket = new WebSocket("ws://localhost:8080","myprotocol");  

url是一个只读属性,返回值为当构造函数创建WebSocket实例对象时URL的绝对路径;
close([code[, reason]])方法:当完成和服务器的通信后,可以调用close()方法关闭Web Socket连接;
code参数是可选的,为一个数字状态码,必须为1000或介于3000和4999之间;如果没有传这个参数,默认使用1006;
参数reason是一个可选的字符串,它解释了连接关闭的原因,但字符串不能超过123个字节;

 socket.close();
console.log(socket.readyState); // 2  

调用了close()之后,会触发onclose事件,并且readyState的值立即变为2(正在关闭),而在关闭连接后就会变成3,如:

 socket.onclose = function(event){
console.log("Was clean? " + event.wasClean + ", Code=" + event.code + ", Reason=" + event.reason + ", readyState=" + socket.readyState);
};  

示例:

 <p><input type="button" id="btnOpen" value="打开WebSocket" />
<input type="button" id="btnClose" value="关闭WebSocket" /></p>
<p><textarea id="message" cols="50" rows="5"></textarea></p>
<p><input type="button" id="btnSend" value="发送消息" /></p>
<script>
var socket = null;
var btnOpen = document.getElementById("btnOpen");
btnOpen.addEventListener("click", function(){
socket = new WebSocket("ws://82.157.123.54:9010/ajaxchattest","myprotocol");
socket.onopen = function(event){
console.log("Connection open.");
socket.send("大师哥王唯");
};
socket.onmessage = function(event){
console.log(event.data);
};
socket.onerror = function(){
console.log("Connection error.");
};
socket.onclose = function(event){
console.log(event);
console.log("wasClean:" + event.wasClean + ", Code:" + event.code + ", Reason:" + event.reason);
};
})
var btnClose = document.getElementById("btnClose");
btnClose.addEventListener("click", function(){
if(socket)
socket.close("3001","就想关闭");
});
var btnSend = document.getElementById("btnSend");
btnSend.addEventListener("click", function(){
var message = document.getElementById("message").value;
if(message && socket && socket.readyState == 1){
socket.send(message);
document.getElementById("message").value = "";
}else{
console.log("请输入内容或打开WebSocket");
}
});
</script>  

服务端的实现:
目前还没有成熟的Web Socket服务器,需要开发者自行实现;所有的后端开发语言都可以实现Web Socket服务端,也有很多开源的第三方库实现了WebSocket,如:

  • µWebSockets:简单、安全且符合标准的web服务器,适用于要求最高的应用程序,其采用C/C++语言编写;地址:
  • socket.io:非常优秀的socket框架,使用Node实现;地址:
  • PyWebSocket:采用Python语言编写,跨平台,扩展简单;
  • WebSocket-Node:采用JavaScript语言编写,其基于nodejs,地址:;
  • LibWebSockets:采用C/C++语言编写,可定制化;
  • ratchet WebSocket(ratchetphp/Ratchet):采用PHP语言开发;

SSE与Web Sockets:
面对某个具体的应用,在考虑是使用SSE还是使用Web Sockets时,可以先考虑到底需不需要双向通信;如果只需要读取服务端数据(如比赛成绩),那么SSE比较容易实现;如果必须双向通信(如聊天室),那么Web Sockets显然更合适;另外,在不能选择Web Sockets的情况下,组合XHR和SSE也是能实现双向通信的;

示例:Web Socket聊天室,client.html:

 <style>
.main{width:80%; margin:50px auto;}
.container{width:100%; height: 300px; border:solid 1px green;}
.onlinediv{width:20%; height: 300px; float: left;}
.msglist{width: 78%; height: 300px; border-left: 1px solid gray; overflow: scroll;float: right;}
h3{text-align: left; padding-left: 20px;}
</style>
<div class="main">
<h1>websocket聊天室</h1>
<div class="container">
<div class="onlinediv">
<h3>当前在线:<span id="user_num">0</span></h3>
<div id="user_list"></div>
</div>
<div id="msg_list" class="msglist">
</div>
</div>
<br>
<textarea id="msg_box" rows="6" cols="50" disabled></textarea><br>
<input id="btn_open" type="button" value="连接" />
<input id="btn_close" type="button" value="断开" disabled />
<input id="btn_send" type="button" value="发送" disabled />
</div>
<script>
var chat = null;
window.onload = function(){
var btn_open = document.getElementById("btn_open");
var btn_close = document.getElementById("btn_close");
var msg_box = document.getElementById("msg_box");
var btn_send = document.getElementById("btn_send");

btn_open.addEventListener("click", openHandler);
btn_close.addEventListener("click", closeHandler);
msg_box.addEventListener("keydown", confirm);
btn_send.addEventListener("click", sendHandler);
}
function openHandler(event){
var uname = prompt('请输入用户名', 'user' + uuid(8, 16));
// var uname = "王唯";
chat = new WebSocket("ws://localhost:8080");
chat.onopen = function () {
var data = "系统消息:建立连接成功";
listMsg(data);
btn_open.setAttribute("disabled",true);
msg_box.removeAttribute("disabled");
btn_close.removeAttribute("disabled");
btn_send.removeAttribute("disabled");
};
chat.onmessage = function (e) {
console.log(e.data);
var data = JSON.parse(e.data);
var sender;

switch (data.type) {
case 'system':
sender = '系统消息: ';
break;
case 'message':
sender = data.from + ': ';
break;
case 'handshake':
var user_info = {'type': 'login', 'content': uname};
sendMsg(user_info);
return;
case 'login': // 登录
case 'logout': // 退出
dealUser(data.content, data.type, data.user_list);
return;
}

var data = sender + data.content;
listMsg(data);
};

chat.onerror = function () {
var data = "系统消息 : 出错了,请退出重试.";
listMsg(data);
};
chat.onclose = function(){
var data = "系统消息 : 已关闭.";
listMsg(data);
btn_open.removeAttribute("disabled");
msg_box.setAttribute("disabled", true);
btn_close.setAttribute("disabled", true);
btn_send.setAttribute("disabled", true);
}
}
function closeHandler(event){
if(chat){
chat.close();
}
}
function confirm(event) {
if (13 == event.keyCode)
sendHandler();
else
return false;
}
function sendHandler() {
var msg_box = document.getElementById("msg_box");
var content = msg_box.value;
content = content.replace(/\r\n/g, "");
var data = {'type': 'message', 'content': content.trim()};
sendMsg(data);
msg_box.value = '';
}
function listMsg(data) {
var msg_list = document.getElementById("msg_list");
var msg = document.createElement("p");
msg.innerHTML = data;
msg_list.appendChild(msg);
msg_list.scrollTop = msg_list.scrollHeight;
}
function dealUser(user_name, type, name_list) {
var user_list = document.getElementById("user_list");
var user_num = document.getElementById("user_num");
while(user_list.hasChildNodes()) {
user_list.removeChild(user_list.firstChild);
}
for (var index in name_list) {
var user = document.createElement("p");
user.innerHTML = name_list[index];
user_list.appendChild(user);
}
user_num.innerHTML = name_list.length;
user_list.scrollTop = user_list.scrollHeight;
var state = type == 'login' ? '上线' : '下线';
var data = '系统消息: ' + user_name + ' 已' + state;
listMsg(data);
}
function sendMsg(data) {
var data = JSON.stringify(data);
chat.send(data);
}
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++)
uuid[i] = chars[0 | Math.random() * radix];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
</script>  

文章来源:智云一二三科技

文章标题:第70节 Server Push服务端推送Comet、SSE和WebSocket-JavaScript

文章地址:https://www.zhihuclub.com/154307.shtml

关于作者: 智云科技

热门文章

网站地图