一直沉溺於兩個客戶端之間的資訊交流,不能自拔
因此寫過兩個版本的網路程式
但都不太滿意,不滿意的有兩點
- 邏輯太複雜
- 不方便迭代開發
用起來心智負擔很高
最近一段時間在整理 reactphp-framework相關的,想搞一些php非同步框架的工具包,已經有好多個包了。
整理過程中,發現mysql-pool連線池的思路對實現兩個客戶端的交流很有啟發。
連線池隱藏了底層的實現細節,只用關心幾個方法就可以了,對mysql來說
實現兩個方法
- query
- queryStream
就能滿足在之上構建想要的東西,比如說ORM.
對與服務端和客戶端交流來說,服務端和客戶端都實現call方法,簡單的呼叫call方法返回讀寫流就能在之上構建網路程式了,類似於這樣。
- 服務端呼叫客戶端
// 執行在服務端 虛擬碼
$pool = new Pool();
// 呼叫客戶端
$stream = $pool->call(function($stream){
// 這裡程式碼執行在客戶端
$stream->on('data',function($data) use ($stream) {
echo $data."\n"; // 收到hello
$stream->end('world');
});
return $stream;
}, $clientId);
$stream->write('hello');
$stream->on('data', function($data){
echo $data."\n"; // 收到world
});
$stream->on('close', function(){
echo "stream close\n";
});
- 客戶端呼叫另外一個客戶端
$client = new Client('server ip');
$stream = $client->call(function($stream){
// 執行在另外一個客戶端
$stream->on('data',function($data) use ($stream) {
echo $data."\n"; // 收到hello
$stream->end('world');
});
return $stream;
}, $peerClientId);
$stream->write('hello');
$stream->on('data', function($data){
echo $data."\n"; // 收到world
});
$stream->on('close', function(){
echo "stream close\n";
});
注意上面兩個例子
- 服務端可以呼叫客戶端
- 客戶端可以呼叫客戶端(透過服務端中轉)
而客戶端不可以呼叫服務端,服務端只是作為流量的中轉,由於閉包裡的程式碼可以自定義,這樣某種程度上保護了服務端
上方的兩個例子,是第三版的核心。在此基礎上,能實現各種各樣的網路程式。
基於此,可以抽象出下方的3種流
第一種 PortToPort
即埠流量轉發,比如將一個客戶端8022埠轉發到另一個客戶端的22埠。(實現ssh到該埠)
<?php
PortToPort::create($client, 'tcp')
// local 8022
->from(null, 8022)
//to another client
->to(
'client_uuid',
'127.0.0.1:22'
)->start();
然後
ssh -p 8022 root@127.0.0.1
就能登入到對端
第二種 StreamToPort
比如已經有了一個流,將這個流指向另一個客戶端的某一埠
use React\Stream\ThroughStream;
$stream = new ThroughStream;
StreamToPort::create($client)
// form one uuid stream
->from('client_uuid', $stream)
->to('client_uuid', '127.0.0.1:8080')
->start();
$stream->write('hello world');
第三種 StreamToStream
已經有了兩個流,將這兩個流量互相轉發,可以對一些流量進行橋接。
$stream1 = new ThroughStream;
$stream2 = new ThroughStream;
StreamToStream::create()->from($stream1)->toStream($stream2);
這三種流其中StreamToStream最為底層,StreamToPort是StreamToStream的上層,而PortToPort是StreamToPort的上層。而能構建出著這三種流,離不開上方的兩個基礎方法。
基於此,所有能抽象成流的資料,都可以將其轉發到某處。
- 客戶端到服務端的流量可以使用tls加密
- 客戶端被呼叫安全性,使用 github.com/laravel/serializable-cl... 的這個包,除非完全信任對端,否則請設定
Client::$secretKey = 'xxxxx';
github.com/reactphp-framework/brid...
gitee.com/reactphp-framework/bridg...
Install
composer require reactphp-framework/bridge dev-master -vvv
Usage
server
<?php
require __DIR__ . '/vendor/autoload.php';
use Reactphp\Framework\Bridge\Server;
use Reactphp\Framework\Bridge\Pool;
use Reactphp\Framework\Bridge\Verify\VerifyUuid;
use Reactphp\Framework\Bridge\DecodeEncode\TcpDecodeEncode;
use Reactphp\Framework\Bridge\TcpBridge;
Server::$debug = true;
$server = new Server(new VerifyUuid([
'8d24e2ba-c6f8-4bb6-8838-cacd37f64165' => '10.10.10.1',//value 是自定義的識別符號,可以是空
'c4b34f0d-44fa-4ef5-9d28-ccef218d74fb' => '10.10.10.2',
'41c5ee60-0628-4b11-9439-a10ba19cbcdd' => '10.10.10.3'
]), new TcpDecodeEncode);
$pool = new Pool($server, [
'max_connections' => 20,
'connection_timeout' => 2,
'keep_alive' => 5,
'wait_timeout' => 3
]);
$tcp = new TcpBridge('0.0.0.0:8010', $server);
client
<?php
require __DIR__ . '/vendor/autoload.php';
use Reactphp\Framework\Bridge\Client;
use Reactphp\Framework\Bridge\DecodeEncode\TcpDecodeEncode;
use React\EventLoop\Loop;
use function React\Async\async;
Client::$debug = true;
$uuid = $argv[1] ?? 'c4b34f0d-44fa-4ef5-9d28-ccef218d74fb';
echo "uuid: $uuid\n";
$uri = 'tcp://192.168.1.9:8010';
$client = new Client($uri, $uuid, new TcpDecodeEncode);
$client->start();
server call client
// 呼叫客戶端
$stream = $pool->call(function($stream){
// 這裡程式碼執行在客戶端
$stream->on('data',function($data) use ($stream) {
echo $data."\n"; // 收到hello
$stream->end('world');
});
return $stream;
}, [
'uuid' => 'c4b34f0d-44fa-4ef5-9d28-ccef218d74fb',
]);
$stream->write('hello');
$stream->on('data', function($data){
echo $data."\n"; // 收到world
});
$stream->on('close', function(){
echo "stream close\n";
});
client call client
$stream = $client->call(function($stream){
// 執行在另外一個客戶端
$stream->on('data',function($data) use ($stream) {
echo $data."\n"; // 收到hello
$stream->end('world');
});
return $stream;
}, [
'uuid' => '8d24e2ba-c6f8-4bb6-8838-cacd37f64165',
// ‘something’ => '10.8.0.1'
]);
$stream->write('hello');
$stream->on('data', function($data){
echo $data."\n"; // 收到world
});
$stream->on('close', function(){
echo "stream close\n";
});
有趣的例子
在上方的服務端配置裡有這樣一段
'8d24e2ba-c6f8-4bb6-8838-cacd37f64165' => '10.10.10.1'
為什麼有ip,php難不成可以轉發ip流量(轉發的是osi第三層,當然第二層也可以轉發)嗎,並實現ip互通嗎,答案是可以的,不過僅限於linux。
- require extensionpecl-tuntap
- if build fail try remove TSRMLS_CC in tuntap.c
下面是個最小demo
server.php
<?php
require __DIR__ . '/vendor/autoload.php';
use Reactphp\Framework\Bridge\Server;
use Reactphp\Framework\Bridge\Pool;
use Reactphp\Framework\Bridge\Verify\VerifyUuid;
use Reactphp\Framework\Bridge\DecodeEncode\TcpDecodeEncode;
use Reactphp\Framework\Bridge\TcpBridge;
use React\EventLoop\Loop;
Server::$debug = true;
$server = new Server(new VerifyUuid([
'8d24e2ba-c6f8-4bb6-8838-cacd37f64165' => '10.10.10.1',
'c4b34f0d-44fa-4ef5-9d28-ccef218d74fb' => '10.10.10.2',
'41c5ee60-0628-4b11-9439-a10ba19cbcdd' => '10.10.10.3'
]), new TcpDecodeEncode);
$pool = new Pool($server, [
'max_connections' => 20,
'connection_timeout' => 2,
'keep_alive' => 5,
'wait_timeout' => 3
]);
$tcp = new TcpBridge('0.0.0.0:8010', $server);
client.php 注意修改裡面的ip
<?php
require __DIR__ . '/vendor/autoload.php';
use Reactphp\Framework\Bridge\Client;
use Reactphp\Framework\Bridge\DecodeEncode\TcpDecodeEncode;
use React\EventLoop\Loop;
use function React\Async\async;
Client::$debug = true;
$uuid = $argv[1] ?? 'c4b34f0d-44fa-4ef5-9d28-ccef218d74fb';
echo "uuid: $uuid\n";
$uri = 'tcp://192.168.1.9:8010';
$client = new Client($uri, $uuid, new TcpDecodeEncode);
$client->start();
function run_command($Command)
{
echo '+ ', $Command, "\n";
$rc = 0;
passthru($Command, $rc);
if ($rc != 0)
echo '+ Command returned ', $rc, "\n";
return ($rc == 0);
}
$client->on('controllerConnected', function ($data) use ($client) {
$ip = $data['something'];
$br = ((php_sapi_name() == 'cli') ? '' : '<br />');
global $TUN;
if (is_resource($TUN)) {
return;
}
// Try to create a new TAP-Device
if (!is_resource($TUN = tuntap_new('', TUNTAP_DEVICE_TUN)))
die('Failed to create TAP-Device' . "\n");
$Interface = tuntap_name($TUN);
echo 'Created ', $Interface, "\n";
run_command('ip link set ' . $Interface . ' up');
run_command("ip addr add $ip/24 dev " . $Interface);
run_command("iptables -t nat -D POSTROUTING -p all -d $ip/24 -j SNAT --to-source $ip");
run_command("iptables -t nat -A POSTROUTING -p all -d $ip/24 -j SNAT --to-source $ip");
// Read Frames from the device
echo 'Waiting for frames...', $br, "\n";
$ipTostreams = [];
Loop::addReadStream($TUN, async(function ($TUN) use ($client, &$ipTostreams) {
// Try to read next frame from device
$Data = $buffer = fread($TUN, 8192);
$Data = substr($Data, 4);
if (($Length = strlen($Data)) < 20) {
trigger_error('IPv4-Frame too short');
return false;
}
// Parse default header
$Byte = ord($Data[0]);
$ipVersion = (($Byte >> 4) & 0xF);
$ipHeaderLength = ($Byte & 0xF);
if ($ipVersion != 4) {
trigger_error('IP-Frame is version ' . $ipVersion . ', NOT IPv4');
return false;
} elseif (($ipHeaderLength < 5) || ($ipHeaderLength * 4 > $Length)) {
trigger_error('IPv4-Frame too short for header');
return false;
}
$ipSourceAddress = (ord($Data[12]) << 24) | (ord($Data[13]) << 16) | (ord($Data[14]) << 8) | ord($Data[15]);
$ipSourceAddress = long2ip($ipSourceAddress);
echo "ipSourceAddress: $ipSourceAddress\n";
$ipTargetAddress = (ord($Data[16]) << 24) | (ord($Data[17]) << 16) | (ord($Data[18]) << 8) | ord($Data[19]);
$ipTargetAddress = long2ip($ipTargetAddress);
echo "ipTargetAddress: $ipTargetAddress\n";
if ($client->getStatus() !== 1) {
echo "client not ready\n";
if (isset($ipTostreams[$ipTargetAddress])) {
echo "close stream\n";
$ipTostreams[$ipTargetAddress]->close();
unset($ipTostreams[$ipTargetAddress]);
}
return;
}
if (isset($ipTostreams[$ipTargetAddress])) {
if ($ipTostreams[$ipTargetAddress] === '') {
echo "stream is connecting\n";
} else {
echo "write to stream\n";
$ipTostreams[$ipTargetAddress]->write($buffer);
}
} else {
echo "create stream\n";
$ipTostreams[$ipTargetAddress] = '';
$stream = $client->call(function ($stream, $info) {
global $TUN;
if (!isset($TUN) || !is_resource($TUN)) {
Loop::futureTick(function () use ($stream) {
$stream->emit('error', [new \Exception('TUN not found')]);
});
return $stream;
}
$stream->on('data', function ($data) use ($TUN) {
fwrite($TUN, $data);
});
return $stream;
}, [
'something' => $ipTargetAddress
]);
$stream->write($buffer);
$stream->on('data', function ($data) use ($TUN) {
fwrite($TUN, $data);
});
$stream->on('error', function ($e) {
echo "file: " . $e->getFile() . "\n";
echo "line: " . $e->getLine() . "\n";
echo $e->getMessage() . "\n";
});
$stream->on('close', function () use (&$ipTostreams, $ipTargetAddress) {
echo "tun stream close\n";
unset($ipTostreams[$ipTargetAddress]);
});
$ipTostreams[$ipTargetAddress] = $stream;
}
}));
});
啟動服務端
php server.php
在兩個客戶端上分別啟動
php client.php c4b34f0d-44fa-4ef5-9d28-ccef218d74fb
php client.php 41c5ee60-0628-4b11-9439-a10ba19cbcdd
驗證
in 10.10.10.3
ping 10.10.10.2
你的linux ip互通後,假如你使用的是windows或mac,和linux在同一網段,可以修改路由策略訪問ip 10.10.10.3
比如在mac上
route -n add -net 10.10.10.3 -netmask 255.255.255.0 '你的linuxip'
然後ping下ip試試,是不是通了?
其它例子
在資料夾下examples
以上
License
MIT
本作品採用《CC 協議》,轉載必須註明作者和本文連結