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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值