您的位置:首页 > Web前端

写给前端工程师的理论基础(3)--websocket这一篇就够了

2017-09-04 23:27 253 查看
在websocket协议没有出来之前,服务器与客户端之间通信的方法比较常用的一种叫做Ajax轮询的方式:

Ajax轮询说白了,就是客户端反复询问的方式,如果服务端有新的内容想要传送给客户端,那么,服务端自然而然就会在客户端轮询的时候,发送其一个有用的数据,这种情况是服务端的有效应答。

而在更多的情况下,这种轮询是低效的,服务端如果没有数据想要发送给客户端,就会返回没有数据的标记字样,这样,长期进行下去,就会导致整个通信系统的效率低下,也就是我们所说的冗余过大。

websocket协议是HTML5附带的一个新的技术福利。

支持全双工的长链接通信


这样,也就可以有效避免了客户端反复询问的问题,也就是我们说的提高了效率。

但是,在设计websocket的时候,也不要什么场合都套用websocket协议,因为websocket协议毕竟是一个长链接的协议形式,所以,我们要充分考虑到我们的业务场景。

我们的业务场景如果需要比较高的及时响应速度,那么我们用websocket是一种很好的选择,如果我们的业务场景对数据相应的时间要求不是很高,或者说,如果从建立websocket长连接开始算起,要有很长时间的等待时延,那么,这种做法无疑不是更好的选择。一般我的业务场景是,5秒钟以上空闲,并且数据交互少于10或更多次的话,就不太需要websocket了。

那么,说完websocket的业务场景,我们简单了解下websocket协议吧,这里面的具体协议例子网上有好多,在这里就挑主要的进行普及性质的讲解:

1.websocket的身份验证方法是通过握手包实现的。

所谓的握手包就是由客户端(浏览器)发起连接请求,由服务器(web容器)相应客户应答,如果客户端判断服务器响应的结果是正确的,那么,二者就算是握手成功了。握手成功之后,就可以双向全双工通信了。

那么,这个握手的过程,实际上是涉及到加密的过程,这个加密过程的大体思路是:

客户端发送base64码的字符串,服务器将该字符串结合一个特定的字符串(我们叫做魔幻字符串,也叫神奇字符串,他的意思就是一个特定的字符串常量),然后进行sha1消息摘要,摘要结果以base64编码的形式返回。

如果你感兴趣的话,你会发现,如果你用在线编码的话,会比这个系统生成的base64编码要长很多,因为,在这个过程中,还要有一些截取的部分。具体的过程,如果感兴趣的话,还可以在网上搜一下php websocket,那里面有php 模仿websocket的实现,看到源代码后,你应该能懂思路了。

2.websocket建立双向全双工通信时,是一个自定义的格式,不是直接往流里面传的哦~

websocket在前端上实现的话,代码特别简单:

<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>Java后端WebSocket的Tomcat实现</title>
</head>
<body>
Welcome<br/><input id="text" type="text"/>
<button onclick="send()">发送消息</button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr/>
<div id="message"></div>
</body>

<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/websocket");
}
else {
alert('当前浏览器 Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
};

//连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
}

//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
}

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}

//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}

//发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>


这是在客户端(浏览器)这么写就行了,那么在服务端呢?

这就有一个很有意思的现象,我们知道php不是常驻内存的,如果超过若干时间(php.conf/php.ini中配置,大概默认60多秒钟)php脚本还在运行,就会被系统自动关闭,这样,就导致了,你想要用:

while(true){}


一直循环等待,就会被系统处理掉。

而且,php默认是不支持websocket的,如果想要用php实现websocket,要写php模拟websocket,写起来很有趣,但是,却可以从源代码中看出来websocket协议的底层实现方法:

class WS {
var $master;  // 连接 server 的 client
var $sockets = array(); // 不同状态的 socket 管理
var $handshake = false; // 判断是否握手

function __construct($address, $port){
// 建立一个 socket 套接字
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
or die("socket_create() failed");
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1)
or die("socket_option() failed");
socket_bind($this->master, $address, $port)
or die("socket_bind() failed");
socket_listen($this->master, 2)
or die("socket_listen() failed");

$this->sockets[] = $this->master;

// debug
echo("Master socket  : ".$this->master."\n");

while(true) {
//自动选择来消息的 socket 如果是握手 自动选择主机
$write = NULL;
$except = NULL;
socket_select($this->sockets, $write, $except, NULL);

foreach ($this->sockets as $socket) {
//连接主机的 client
if ($socket == $this->master){
$client = socket_accept($this->master);
if ($client < 0) {
// debug
echo "socket_accept() failed";
continue;
} else {
//connect($client);
array_push($this->sockets, $client);
echo "connect client\n";
}
} else {
$bytes = @socket_recv($socket,$buffer,2048,0);
print_r($buffer);
if($bytes == 0) return;
if (!$this->handshake) {
// 如果没有握手,先握手回应
$this->doHandShake($socket, $buffer);
echo "shakeHands\n";
} else {

// 如果已经握手,直接接受数据,并处理
$buffer = $this->decode($buffer);
//process($socket, $buffer);
echo "send file\n";
}
}
}
}
}

function dohandshake($socket, $req)
{
// 获取加密key
$acceptKey = $this->encry($req);
$upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " . $acceptKey . "\r\n" .
"\r\n";

echo "dohandshake ".$upgrade.chr(0);
// 写入socket
socket_write($socket,$upgrade.chr(0), strlen($upgrade.chr(0)));
// 标记握手已经成功,下次接受数据采用数据帧格式
$this->handshake = true;
}

function encry($req)
{
$key = $this->getKey($req);
$mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}

function getKey($req)
{
$key = null;
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $req, $match)) {
$key = $match[1];
}
return $key;
}

// 解析数据帧
function decode($buffer)
{
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;

if ($len === 126)  {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else if ($len === 127)  {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else  {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}

// 返回帧信息处理
function frame($s)
{
$a = str_split($s, 125);
if (count($a) == 1) {
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o) {
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}

// 返回数据
function send($client, $msg)
{
$msg = $this->frame($msg);
socket_write($client, $msg, strlen($msg));
}
}

测试    $ws = new WS("127.0.0.1",2000);


所以,一般想要实现websocket的好办法,就是用java来实现。

java 的jsp就可以写websocket,写法比较常用的是引入tomcat Lib目录下,有关websocket的两个jar包(有且仅有两个jar包包含websocket关键字),然后通过注解的方式来写就ok了。

还有一种解决方案,就是自己造轮子,写一个专门应答websocket的服务器组件,这个写起来乍一看不复杂,其实如果想要充分利用面向对象,实现高并发、高可用还是挺有难度的~

后续,我也会造一个轮子,来实现这个功能,并分享出来~!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: