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方法中返回的值。