如果说我看得比别人远一些,那是因为我站在巨人的肩膀上 --艾萨克·牛顿
技术背景
今天在优化现有游戏GM系统(以下简称系统)时候,出现一个需求:低级管理员(以下简称A)请求危险URL时候需要高级管理员(以下简称B)授权才可以。那么在老版本系统中,我的解决方法是当B遇到这样的情况时候,人工将这个情况告诉A通过qq等手段,然后A做出相应。这显然是一个较为麻烦和不可靠的方法,所以,新版本中将通过系统发送通知的形式来实现这个需求。粗略考虑,两种解决方案:
- 使用JS,setTimeout() 方法不断的异步请求数据,间隔时间可以是10秒或者更短,主要看需求。
- 使用WebSocket,由服务器主动向客户端发送数据。[我选择用它]
前期准备
在明确了前文的需求之后,对前文所提的方案进行比较,最后还是选择了2,因为,可以借机熟悉下WebSocket这门技术。好了废话不多说,看过程吧!
- 一台Linux服务器[最好可以访问国外,你懂得]
- 安装好Web服务器及PHP环境
- 安装好
nvm
[用于管理nodeJs的工具包]
搭建WebSocket服务
这里要说明的是,我们将采用第三种方式来实现我们的WebSocket服务。请参看
- 安装WebSocket服务
去github上搜索下,有很多直接将PHP作为一个Socket服务端来做,但是,我在考虑速度和效率以及开发周期这些因素后,我放弃了这么做。因为说实话PHP对于Socket的支持并没有那么好。所以将使用node
下的ws
先来做一个Socket服务端。安装步骤如下:
nvm install v4.3 //安装Node.js
npm install ws //安装Socket服务端
vim server.js //配置Socket服务器
node server.js //启动Socket服务器
/** 一般的Socket服务器需要一直运行,即便我们关闭了terminal **/
npm install -g forever //全局模式安装forever,用这个来运行server.js
forever start server.js //运行服务端
forever list //查看forever管理的全部任务
附上我的server.js的配置源码:
console.log("Server started");
//TODO::发呆过久踢下线
var id2Obj = new Array(); //根据用户ID找到的目标socket通道
var lv2Id = new Array(); //根据这个随机出目标管理员推送消息
var WebSocketServer = require('ws').Server;
var webSocket = new WebSocketServer({port: 8010});
webSocket.on('connection', function(ws) {
ws.on('message', function(message) {
var msgObj = JSON.parse(message);
if( msgObj.type == 'OPEN' ){
id2Obj[msgObj.content] = ws;
if( msgObj.lv == 'AUTH' ){
lv2Id[msgObj.content] = msgObj.content;
}
}
if( msgObj.type == 'CLOSE' ){
id2Obj[msgObj.content] = null;
}
if( msgObj.type == 'RULE' ){
for (var key in lv2Id) {
tar = id2Obj[key];
tar.send('ADD NEW MESSAGE');
}
}
// ws.send('Server received from client: ' + message);
});
});
- 创建PHP Socket客户端
这里主要参看的是github项目,在这里就不在赘述了。
附上我优化后的代码
<?php
namespace MyOrg\WebSocket;
use MyOrg\String\StringOrg;
/**
* Class Client
* @package MyOrg\WebSocket
* @auth https://github.com/Textalk/websocket-php
*/
class Client extends Base{
protected $socket_uri;
public $options;
public function __construct ($uri, $options = array()) {
$this->options = $options;
if (!array_key_exists('timeout', $this->options)) $this->options['timeout'] = 5;
if (!array_key_exists('fragment_size', $this->options)) $this->options['fragment_size'] = 4096;
$this->socket_uri = $uri;
}
public function __destruct (){
if ($this->socket) {
if (get_resource_type($this->socket) === 'stream') fclose($this->socket);
$this->socket = null;
}
}
protected function connect (){
$url_parts = parse_url($this->socket_uri);
$scheme = $url_parts['scheme'];
$host = $url_parts['host'];
$user = isset($url_parts['user']) ? $url_parts['user'] : '';
$pass = isset($url_parts['pass']) ? $url_parts['pass'] : '';
$port = isset($url_parts['port']) ? $url_parts['port'] : ($scheme === 'wss' ? 443 : 80);
$path = isset($url_parts['path']) ? $url_parts['path'] : '/';
$query = isset($url_parts['query']) ? $url_parts['query'] : '';
$fragment = isset($url_parts['fragment']) ? $url_parts['fragment'] : '';
$path_with_query = $path;
if (!empty($query)) $path_with_query .= '?' . $query;
if (!empty($fragment)) $path_with_query .= '#' . $fragment;
if (!in_array($scheme, array('ws', 'wss'))) {
E("Url should have scheme ws or wss, not '$scheme' from URI '$this->socket_uri' .");
}
$host_uri = ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host;
if (isset($this->options['context'])) {
if (@get_resource_type($this->options['context']) === 'stream-context') {
$context = $this->options['context'];
} else {
throw new \InvalidArgumentException(
"Stream context in \$options['context'] isn't a valid context"
);
}
} else {
$context = stream_context_create();
}
$this->socket = @stream_socket_client(
$host_uri . ':' . $port,
$errno,
$errstr,
$this->options['timeout'],
STREAM_CLIENT_CONNECT,
$context
);
if ($this->socket === false) {
E("Could not open socket to \"$host:$port\": $errstr ($errno).");
}
stream_set_timeout($this->socket, $this->options['timeout']);
$key = base64_encode(StringOrg::randString(16,0,'!"$&/()=[]{}0123456789'));
$headers = array(
'host' => $host . ":" . $port,
'user-agent' => 'websocket-client-php',
'connection' => 'Upgrade',
'upgrade' => 'websocket',
'sec-websocket-key' => $key,
'sec-websocket-version' => '13',
);
if ($user || $pass) {
$headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass) . "\r\n";
}
if (isset($this->options['origin'])) {
$headers['origin'] = $this->options['origin'];
}
if (isset($this->options['headers'])) {
$headers = array_merge($headers, array_change_key_case($this->options['headers']));
}
$header =
"GET " . $path_with_query . " HTTP/1.1\r\n"
. implode(
"\r\n", array_map(
function ($key, $value) {
return "$key: $value";
}, array_keys($headers), $headers
)
)
. "\r\n\r\n";
$this->write($header);
$response = stream_get_line($this->socket, 1024, "\r\n\r\n");
if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
$address = $scheme . '://' . $host . $path_with_query;
E("Connection to '{$address}' failed: Server sent invalid upgrade response:\n" . $response);
}
$keyAccept = trim($matches[1]);
$expectedResonse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
if ($keyAccept !== $expectedResonse) {
E('Server sent bad upgrade response.');
}
$this->is_connected = true;
}
}
<?php
namespace MyOrg\WebSocket;
/**
* Class Base
* @package MyOrg\WebSocket
* @auth https://github.com/Textalk/websocket-php
*/
class Base {
protected $socket;
protected $is_connected = false;
protected $is_closing = false;
protected $last_opcode = null;
protected $close_status = null;
protected $huge_payload = null;
public $options;
protected static $opcodes = array(
'continuation' => 0,
'text' => 1,
'binary' => 2,
'close' => 8,
'ping' => 9,
'pong' => 10,
);
protected function connect(){}
public function getLastOpcode(){
return $this->last_opcode;
}
public function getCloseStatus(){
return $this->close_status;
}
public function isConnected(){
return $this->is_connected;
}
public function setTimeout($timeout){
$this->options['timeout'] = $timeout;
if ($this->socket && get_resource_type($this->socket) === 'stream') {
stream_set_timeout($this->socket, $timeout);
}
}
public function setFragmentSize($fragment_size){
$this->options['fragment_size'] = $fragment_size;
return $this;
}
public function getFragmentSize(){
return $this->options['fragment_size'];
}
public function send($payload, $opcode = 'text', $masked = true){
if (!$this->is_connected) $this->connect();
if (!in_array($opcode, array_keys(self::$opcodes))) {
E("Bad opcode '$opcode'. Try 'text' or 'binary'.");
}
$payload_length = strlen($payload);
$fragment_cursor = 0;
while ($payload_length > $fragment_cursor) {
$sub_payload = substr($payload, $fragment_cursor, $this->options['fragment_size']);
$fragment_cursor += $this->options['fragment_size'];
$final = $payload_length <= $fragment_cursor;
$this->send_fragment($final, $sub_payload, $opcode, $masked);
$opcode = 'continuation';
}
}
protected function send_fragment($final, $payload, $opcode, $masked){
$frame_head_binstr = '';
$frame_head_binstr .= (bool)$final ? '1' : '0';
$frame_head_binstr .= '000';
$frame_head_binstr .= sprintf('%04b', self::$opcodes[$opcode]);
$frame_head_binstr .= $masked ? '1' : '0';
$payload_length = strlen($payload);
if ($payload_length > 65535) {
$frame_head_binstr .= decbin(127);
$frame_head_binstr .= sprintf('%064b', $payload_length);
} elseif ($payload_length > 125) {
$frame_head_binstr .= decbin(126);
$frame_head_binstr .= sprintf('%016b', $payload_length);
} else {
$frame_head_binstr .= sprintf('%07b', $payload_length);
}
$frame = '';
foreach (str_split($frame_head_binstr, 8) as $binstr){
$frame .= chr(bindec($binstr));
}
if ($masked) {
$mask = '';
for ($i = 0; $i < 4; $i++) {
$mask .= chr(rand(0, 255));
}
$frame .= $mask;
}
for ($i = 0; $i < $payload_length; $i++) {
$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
}
$this->write($frame);
}
public function receive(){
if (!$this->is_connected) $this->connect();
$this->huge_payload = '';
$response = null;
while (is_null($response)) {
$response = $this->receive_fragment();
}
return $response;
}
protected function receive_fragment(){
$data = $this->read(2);
$final = (boolean)(ord($data[0]) & 1 << 7);
$rsv1 = (boolean)(ord($data[0]) & 1 << 6);
$rsv2 = (boolean)(ord($data[0]) & 1 << 5);
$rsv3 = (boolean)(ord($data[0]) & 1 << 4);
$opcode_int = ord($data[0]) & 31;
$opcode_ints = array_flip(self::$opcodes);
if (!array_key_exists($opcode_int, $opcode_ints)) {
E("Bad opcode in websocket frame: $opcode_int");
}
$opcode = $opcode_ints[$opcode_int];
if ($opcode !== 'continuation') {
$this->last_opcode = $opcode;
}
$mask = (boolean)(ord($data[1]) >> 7);
$payload = '';
$payload_length = (integer)ord($data[1]) & 127;
if ($payload_length > 125) {
if ($payload_length === 126) $data = $this->read(2);
else $data = $this->read(8);
$payload_length = bindec(self::sprintB($data));
}
if ($mask) {
$masking_key = $this->read(4);
}
if ($payload_length > 0) {
$data = $this->read($payload_length);
if ($mask) {
for ($i = 0; $i < $payload_length; $i++) {
$payload .= ($data[$i] ^ $masking_key[$i % 4]);
}
} else {
$payload = $data;
}
}
if ($opcode === 'close') {
if ($payload_length >= 2) {
$status_bin = $payload[0] . $payload[1];
$status = bindec(sprintf("%08b%08b", ord($payload[0]), ord($payload[1])));
$this->close_status = $status;
$payload = substr($payload, 2);
if (!$this->is_closing) {
$this->send($status_bin . 'Close acknowledged: ' . $status, 'close', true);
}
}
if ($this->is_closing) {
$this->is_closing = false;
}
fclose($this->socket);
$this->is_connected = false;
}
if (!$final) {
$this->huge_payload .= $payload;
return null;
} else if ($this->huge_payload) {
$payload = $this->huge_payload .= $payload;
$this->huge_payload = null;
}
return $payload;
}
public function close($status = 1000, $message = 'ttfn'){
$status_binstr = sprintf('%016b', $status);
$status_str = '';
foreach (str_split($status_binstr, 8) as $binstr) $status_str .= chr(bindec($binstr));
$this->send($status_str . $message, 'close', true);
$this->is_closing = true;
$response = $this->receive();
return $response;
}
protected function write($data){
$written = fwrite($this->socket, $data);
if ($written < strlen($data)) {
E("Could only write $written out of " . strlen($data) . " bytes.");
}
}
protected function read($length){
$data = '';
while (strlen($data) < $length) {
$buffer = fread($this->socket, $length - strlen($data));
if ($buffer === false) {
$metadata = stream_get_meta_data($this->socket);
E('Broken frame, read ' . strlen($data) . ' of stated ' . $length . ' bytes. Stream state: ' . json_encode($metadata));
}
if ($buffer === '') {
$metadata = stream_get_meta_data($this->socket);
E('Empty read; connection dead? Stream state: ' . json_encode($metadata));
}
$data .= $buffer;
}
return $data;
}
protected static function sprintB($string){
$return = '';
for ($i = 0; $i < strlen($string); $i++) {
$return .= sprintf("%08b", ord($string[$i]));
}
return $return;
}
}
- 创建JS Socket客户端
这里网上也有很多的源码,百度下就可以。
到这里,基本配置都已经完成,下面要做的就是测试啦,各位试试吧,不过要注意的是我这些代码并不具备通用性,如果想要探讨,欢留言哦!
最后说两句,至于我为什么敢于去弄一个Node.js的Socket服务端,主要是因为老大。没有他的帮助我基本不可能这么快的上手,有兴趣也开以去看看他的博客。
@toby
,对了,我基本不懂JS,但是我也能写点出来,所以如果只是想用下,根本不用担心学不会。加油!!!
本文由 陌上花开 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jul 1, 2016 at 04:05 am