websocket协议

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。

WebSocket通讯分为握手阶段、二进制通讯阶段。 一个典型的WebSocket握手请求信息:


GET /webfin/websocket/ HTTP/1.1
Host: localhost  
Upgrade: websocket  
Connection: Upgrade Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg== Origin: http://referer-url Sec-WebSocket-Version: 13
服务器响应

HTTP/1.1 101 Switching Protocols  
Upgrade: websocket  
Connection: Upgrade Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

WebSocket借用http请求进行握手,相比正常的http请求,多了一些内容。其中,
Upgrade: websocket
Connection: Upgrade
表示希望将http协议升级到Websocket协议。
Sec-WebSocket-Key是浏览器随机生成的base64 encode的值,用来询问服务器是否是支持WebSocket。

服务器返回:
Upgrade: websocket
Connection: Upgrade

告诉浏览器即将升级的是Websocket协议。
Sec-WebSocket-Accept是将请求包“Sec-WebSocket-Key”的值,与”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″这个字符串进行拼接,然后对拼接后的字符串进行sha-1运算,再进行base64编码得到的。用来说明自己是WebSocket助理服务器。

Sec-WebSocket-Version是WebSocket协议版本号。RFC6455要求使用的版本是13,之前草案的版本均应当被弃用。

更多握手规范详见RFC6455。

beyod内置了WebSocket服务器支持,我们可以快速实现一个WebSocket服务器:

配置文件

在配置文件config/main.php的server组件的listenners中配置:

//...
'listeners' => [
    /...
    'ws' => [ //ws是自定义的名称,用于标识当前侦听器
        'class' => 'tcp://0.0.0.0:813',
        'parser' => 'beyod\protocol\websocket\Parser',
        'handler' => 'beyod\protocol\websocket\Handler',//应该定义自己的handler并重写相关事件方法实现相关业务功能。
    ]
]

webosocket的握手功能,已经实现,只需要实现具体的业务事件回调即可:

<?php
/**
 * @link http://www.beyo.io/
 * @copyright Copyright (c) 2017 Beyo.IO Software Team.
 * @license http://www.beyo.io/license/
 */

namespace beyod\protocol\websocket;

use beyod\Connection;
use beyod\MessageEvent;
use Yii;
use beyod\ErrorEvent;
use beyod\protocol\http\Request as HttpRequest;
use beyod\protocol\http\Response as HttpResponse;


/**
 * websocket request handler class.
 * @see http://www.beyo.io/document/class/protocol-websocket
 * @author zhang xu <zhangxu@beyo.io>
 * @since 1.0
 */

class Handler extends \beyod\Handler
{
    //http握手阶段完成之后,就需要把客户端的Sec-Key保存,以作好状态登记。
    public $secKey = 'ws-seckKey';
    
    public $magic_code = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
    
    public $headers = [
        'Connection' => 'Upgrade',
        'Upgrade'   => 'WebSocket',
        'Access-Control-Allow-Credentials' => 'true',
        'Access-Control-Allow-Headers' => 'content-type'
    ];
    
    
    /**
     * 当收到的客户端数据包时的回调, 根据响应消息的类型,判断是握手或是二进制传输阶段。具体实现请参阅
     beyod\protocol\websocket\Parser::decode
     
     * @param MessageEvent $event
     * 
     * @see Parser::decode
     */
    public function onMessage(MessageEvent $event){
        if($event->message instanceof HttpRequest){
            return $this->processHandshake($event);
        }
        
        if(!($event->message instanceof Request)){
            Yii::error("message for ".get_class($this).'::onMessage must be StreamRequest');
            return ;
        }
        
        return $this->processStream($event);
    }
    
    /**
    * 当收到http握手请求时,发送相应的握手响应, 这部分一般无法重写
    */
    public function processHandshake(MessageEvent $event)
    {
        /** @var \beyod\protocol\http\Request $message */
        $message = $event->message;
        
        $resp = new HttpResponse(101);
        foreach($this->headers as $name => $value){
            $resp->headers->set($name, $value);
        }
        
        $magicValue = base64_encode(sha1($event->message->headers->get('Sec-Websocket-Key').$this->magic_code,true));
        $resp->headers->set('Sec-Websocket-Accept', $magicValue);
        
        $event->sender->setAttribute($this->secKey, $magicValue);
        
        $event->sender->send($resp);
        
        $this->onHandshaked($event);
    }
    
    /**
    * http握手完成之后的回调,如通知其它客户端,或向客户端发送消息。
    */
    public function onHandshaked($event)
    {
        
    }
    
    /**
     * 当收到二进制数据包时的回调
     * 
     * @param MessageEvent $event
     */
    
    public function processStream($event)
    {
        /**
         * @var Request $event->message
         */
        
        //控制指令 作相应的响应即可。
        if($event->message->isCtlFrame()) {
            if($event->message->isCloseFrame()) {
                return $this->processClose($event);
            }else if($event->message->isPingFrame()){
                return $this->processPing($event);
            }else if($event->message->isPongFrame()){
                return $this->processPong($event);
            }
            
        }else{
            return $this->processData($event);
        }
    }
    
    /**
     * 关闭数据包实现
     * @param MessageEvent $event
     */
    public function processClose($event)
    {
        $response = new Response();
        $response->opcode = Request::OPCODE_CLOSE;
        return $event->sender->close($response);
    }
    
    /**
     * ping响应
     * @param MessageEvent $event
     */
    public function processPing($event)
    {
        $res = new Response();
        $res->fin = 1;
        $res->opcode = Request::OPCODE_PONG;
        $res->mask=0;
        $res->payload_len=0;
        $event->sender->send($res);
    }
    
    /**
     * process pong request
     * @param MessageEvent $event
     */
    public function processPong($event)
    {
        
    }
    
    /**
     * 处理正文数据包,一般需要重写此方法,实现业务功能。
     * 
     * @tutorial  $event->message Request
     * @tutorial  $event->message->body string
     * @tutorial  $event->message->opcode int
     * 
     * @param MessageEvent $event
     */
    public function processData($event)
    {
        
    }
    
    /*
    * 当发生错误时,可向客户端发送相应的响应。
    */
    public function sendErrorResponse(ErrorEvent $event)
    {
        if($event->sender->hasAttribute($this->secKey)) {
            
        }else{
            parent::sendErrorResponse($event);
        }
    }
}

beyod\protocol\websocket\Response beyod\protocol\websocket\Request分别代表了websocket二进制数据包的请求和响应结构封装,它是由Parser的decode方法中返回的值。