WebSocket服务端开发(一)-WebSocketServer类简介

本文介绍使用PHP语言编写的Server类WebSocketServer的整体设计思路及包含的方法。WebSocketServer类只是一个简单的单线程的WebSocket基类,可以通过继承该类实现自己的Server类。

一、成员变量
包含以下成员变量:

//各种帧类型的常量
const FRAME_CONTINUE = 0x00;
const FRAME_TEXT = 0x01;
const FRAME_BIN = 0x02;
const FRAME_CLOSE = 0x08;
const FRAME_PING = 0x09;
const FRAME_PONG = 0x0A;

protected $serverSocket = null;//服务器监听的socket
protected $shutdown = false;//关闭状态,如果是true表示服务器准备关闭
protected $socketList = array();//保存所有socket的数组
protected $socketListMap = array();//根据唯一id对socket进行索引,并保存socket的其他自定义属性,相当于session
private $handshakingList = array();//正在进行握手的socket,用于处理握手超时的socket
private $lastHealthCheck = null;//最后一次进行健康检查的时间,这里根据最后一次通信时间判断健康状态,检查时默认不会发送pong帧
private $healthCheckInterval = 300;//健康检查间隔,单位秒,每次处理完一个连接后会判断是否进行健康检查。
private $handshakeTimeout = 10;//握手超时时间,单位秒,为了防止过多的未完成握手占用系统资源,会对超时的握手连接进行关闭处理。

二、构造函数与析构函数

//port监听的端口号,address监听的IP地址,0.0.0.0表示监听本机上任何地址,debug为调试开关,为true是会打印错误信息及其他信息。
function __construct($port, $address = '0.0.0.0', $debug = false) {
  $this->serverSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
  socket_set_option($this->serverSocket, SOL_SOCKET, SO_REUSEADDR, 1);
  socket_set_option($this->serverSocket, SOL_SOCKET, TCP_NODELAY, 1);
  //socket绑定
  if (socket_bind($this->serverSocket, $address, $port) === false) {
    if ($debug) {
      echo $this->getLastErrMsg();
    }
    return;
  }

  //监听开始
  if (socket_listen($this->serverSocket) === false) {
    if ($debug) {
      echo $this->getLastErrMsg();
    }
    return;
  }

  $this->onstarted($this->serverSocket);
  $this->lastHealthCheck = time();
  $this->run();
}

function __destruct() {
  socket_close($this->serverSocket);
}

三、WebSocket类方法

//主工作函数,控制整体流程
function run(){}
//服务启动后回调
function onstarted($serverSocket){}
//socket连接后回调
function onconnected($socket){}
//收到不完整的握手包时回调
function onUpgradePartReceive($socketId){}
//握手失败后回调
function onHandShakeFailure($socketId){}
//握手成功后回调
function onHandShakeSuccess($socketId){}
//断开连接时回调
function ondisconnected($socketId){}
//从列表中移除socket时回调
function onAfterRemoveSocket($socketId){}
//健康检查后回调
function onafterhealthcheck($unhealthyList){}
//遇到socket错误时回调
function onerror($errCode, $socketId){}
//服务器关闭时回调
function onshutdown(){}

//健康检查函数
function healthCheck(){}
//业务逻辑处理函数(应由继承类覆盖实现)
function businessHandler($socketId){}
//帧是否结束
function isFin($byte){}
//帧是否进行掩码处理
function isMasked($byte){}
//获取帧类型
function getFrameType($byte){}
//处理任意类型的帧
function parseRawFrame($payload, $mask){}
//处理文本帧
function parseTextFrame($payload, $mask){}
//处理二进制帧
function parseBinaryFrame($payload, $mask){}
//创建关闭帧,支持关闭码及关闭原因
function closeFrame($socketId, $closeCode = 1000, $closeMsg = 'goodbye'){}
//发送ping帧
function sendPing($socketId, $data = 'ping'){}
//发送pong帧
function sendPong($socketId, $data = 'pong'){}
//获取掩码
function getMask($data, $len){}
//获取数据负载
function getPayload($data, $len){}
//获取负载长度
function getPayloadLen($data){}
//判断是否为控制帧
function isControlFrame($frameType){}
//解析帧
function parseFrame($data, $socketId){}
//创建指定类型的帧,支持分帧
function createFrame($data, $type, $fin = 0x01){}
//计算WebSocket-Accept值
function getWebSocketAccept($websocketKey){}
//协议选择(协商),建议继承后通过覆盖自定义
function selectProtocol($protocols){}
//获取握手响应头
function getHandShakeHeader($headers){}
//基础协议头检查,只检查WebSocket协议要求的请求头字段
function checkBaseHeader($header){}
//用户自定义头检查,如cookie等信息检查,需要覆盖此方法实现,该处返回恒为true
function checkCustomHeader($header){}
//创建握手失败的响应
function badRequest($socketId){}
//获取响应头的各个字段
function getHeaders($header){}
//移除握手超时的连接
function removeUnhandshakeConnect(){}
//WebSocket握手
function doHandShake($socketId){}
//显示数据信息,仅调试使用
function showData($buffer){}
//发送数据
function sendData($socketId, $data, $type = self::FRAME_TEXT, $isFin = true){}
//根据socket获取socketId
function getSocketId($socket){}
//添加socket到列表中
function addSocket($socket){}
//从列表中移除socket
function removeSocket($socketId){}
//关闭服务器
function shutdown(){}
//接受Socket
function socketAccept(){}
//从socket中读数据
function socketRecv($socketId){}
//通过socket写数据
function socketSend($socketId, $data){}
//关闭底层socket
function socketClose($socketId){}
//连接socket操作
function connect($socket){}
//通过socket断开连接
function disconnectBySocket($socket){}
//通过socketId断开连接
function disconnect($socketId, $silent = false){}
//获取错误码
function getLastErrCode($socketId = null){}
//获取错误详情
function getLastErrMsg($socketId = null, $errCode = null){}

后面的文章将具体介绍每个函数的实现。

WebSocket协议详解及应用(七)-WebSocket协议关闭帧

本文介绍WebSocket协议的关闭帧,包括客户端及服务器如何发送并处理关闭帧、关闭帧错误码及错误处理方法。

一、关闭WebSocket连接

要断开WebSocket连接,需要一个端点断开底层的TCP连接。端点需要通过某种方式来完全关闭TCP连接,例如TLS会话,并适当的丢弃未接收完毕的数据。端点也在必要时可以通过一些有效的方式断开连接,如在受到攻击时。
在一般情况下,底层TCP连接应先被服务端断开,以便保持TIME_WAIT状态。这是为了防止其在2个最大分节生命期(1~4分钟,Windows操作系统为4分钟)之内重新打开,否则可能会由于接到一个高序列的SYN包而重新打开连接。在一些异常的情况下(如在一段时间内未收到服务器端TCP关闭帧),客户端可以关闭TCP连接。如果服务器发出关闭指令,则它需要立即关闭连接。而客户端发出关闭指令需要等待服务器发送的TCP关闭帧。

二、关闭握手阶段

关闭握手阶段需要一个状态码和一个可选的关闭原因,端点必须发送一个关闭控制帧,并设置状态码和关闭原因。一旦端点发送并接收了关闭帧,就需要按上节中的方法关闭WebSocket连接。

三、关闭握手阶段开始

当接收或发送关闭帧后,代表关闭握手阶段开始,此时WebSocket连接进入到CLOSING状态。

四、关闭WebSocket连接结束

当底层TCP连接关闭时,代表WebSocket连接已关闭,此时WebSocket连接状态改为CLOSED。如果TCP连接在WebSocket关闭握手结束后断开,则此次WebSocket为一次完整的(cleanly)关闭。
如果WebSocket连接未能建立,它仍叫做连接关闭,但不是完整的。

五、关闭码

关闭帧可以包含一个关闭码和一个关闭原因。关闭帧可以由任何一方发起,也可以双方同时发起。若关闭帧没有指明关闭码,则认为关闭码为1005,如果WebSocket连接断开,而没有任何关闭帧(如底层传输时丢帧),则认为关闭码为1006。
注意:双方发送的关闭码可能不一致。例如,对方发送了一个关闭帧,但本地程序还没有将数据及关闭帧从socket接收缓存中读取出来,然后本地程序决定发送一个关闭帧,双方都会发送并接收到一个关闭帧并不会再次发送关闭帧(即只进行一次收发,即使不是发过关闭帧后收到的帧)。

六、关闭原因

关闭原因是可选的,跟在关闭码后面,为UTF-8编码的数据,并未对其内容做详细的定义。如果没有设置关闭原因,则关闭原因是一个空的字符串。
注意:同关闭码一样,双方发送的关闭原因可能不一致。

七、强制关闭连接

一些情况会引起强制关闭连接,当情况发生时,客户端需要关闭连接并将错误返回给用户(如控制台中报错等),同样,服务器需要关闭连接并将问题记录在日志中。
如果WebSocket连接建立在端点需要强制关闭连接之前,端点需要在处理关闭帧之前发送关闭帧并发送正确的关闭码。当强制关闭连接后,端点不能再次尝试向对方发送任何数据(包括关闭帧)。
除了上述情况或指定的应用层协议(如WebSocket API)外,客户端不应该断开连接。

八、关闭码

1000 正常关闭
1001 端点丢失,如服务器宕机或浏览器切换其他页面
1002 协议错误
1003 数据类型错误(例如端点只能处理文本,但传来了二进制消息)
1004 保留
1005 保留,禁止由端点发送此类型关闭帧,它是用来当端点没有表明关闭码时的默认关闭码。
1006 保留,禁止由端点发送此类型关闭帧,它是用来当端点未发送关闭帧,连接异常断开时使用。
1007 数据内容错误(如在text帧中非utf-8编码的数据)
1008 端点已接收消息,但违反其策略。当没有更好的关闭码(1003或1009)的时候用此关闭码或者不希望显示错误细节。
1009 内容过长
1010 客户端期望服务器协商一个或多个扩展,但这些扩展并未在WebSocket握手响应中返回。
1011 遇到未知情况无法执行请求
1015 保留,禁止由端点发送此类型关闭帧,它会在TLS握手失败(如证书验证失败)时返回。
保留关闭码
0-999 尚未使用
1000-2999 协议保留,用于未来版本、扩展等
3000-3999 为库、框架、应用程序保留,这些状态码可在IANA中注册,这些状态码并未在此协议中实现。
4000-4999 私有保留,不可被注册。用于开发者自定义关闭码。