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

来源:互联网 发布:java过滤器工作原理 编辑:程序博客网 时间:2024/06/10 14:09

在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的服务器组件,这个写起来乍一看不复杂,其实如果想要充分利用面向对象,实现高并发、高可用还是挺有难度的~
后续,我也会造一个轮子,来实现这个功能,并分享出来~!