<p style="text-align: left; padding-bottom: 0px; widows: 1; text-transform: none; background-color: rgb(255,255,255); text-indent: 0px; margin: 8px 0px; padding-left: 0px; letter-spacing: 0px; padding-right: 0px; font: 12px/22px 'Microsoft YaHei',Verdana,sans-serif,宋体; word-wrap: break-word; white-space: normal; color: rgb(51,51,51); word-break: break-all; word-spacing: 0px; padding-top: 0px; -webkit-text-stroke-width: 0px">前两天在B站上看到一个小伙纸100元组装个电脑打LOL画质流畅,突发奇想100行代码能(简单)实现个啥好玩的。我主要是做PHP开发的,于是就有了本文。
<p style="text-align: left; padding-bottom: 0px; widows: 1; text-transform: none; background-color: rgb(255,51); word-break: break-all; word-spacing: 0px; padding-top: 0px; -webkit-text-stroke-width: 0px">当然,由于PHP(不算swoole扩展)本身不擅长做网络服务端编程,所以这个代理,只是个玩具,离日常使用有点距离。如果想使用稳定可靠的加密(所以能禾斗学上网)代理,可以用这个:<a style="padding-bottom: 0px; margin: 0px; outline-style: none; outline-color: invert; padding-left: 0px; outline-width: 0px; padding-right: 0px; color: rgb(255,131,115); font-size: 12px; padding-top: 0px" href="https://github.com/momaer/asocks-go">https://github.com/momaer/asocks-go也是100来行代码使用go实现。
<p style="text-align: left; padding-bottom: 0px; widows: 1; text-transform: none; background-color: rgb(255,51); word-break: break-all; word-spacing: 0px; padding-top: 0px; -webkit-text-stroke-width: 0px">写的过程中发现php多线程还是难的。比如我开始想每个连接新建一个线程。但这个线程得保存起来(比如保存到数组),比如官方例子中的这个:<a style="padding-bottom: 0px; margin: 0px; outline-style: none; outline-color: invert; padding-left: 0px; outline-width: 0px; padding-right: 0px; color: rgb(255,115); font-size: 12px; padding-top: 0px" href="https://github.com/krakjoe/pthreads/blob/master/examples/SocketServer.php">https://github.com/krakjoe/pthreads/blob/master/examples/SocketServer.php<span class="Apple-converted-space">要放到$clients这个数组里,不然,你试试(curl -L一个要301的地址)就知道出现什么情况了。
<p style="text-align: left; padding-bottom: 0px; widows: 1; text-transform: none; background-color: rgb(255,51); word-break: break-all; word-spacing: 0px; padding-top: 0px; -webkit-text-stroke-width: 0px">这个例子说了in the real world,do something here to ensure clients not running are destroyed 但是,如何把不再运行的连接销毁却没有讲。恩。我试了把$clients放到一个类里,把类传给线程类,然后在线程类要结束时把$clients里对应的连接给unset掉,无果。
<p style="text-align: left; padding-bottom: 0px; widows: 1; text-transform: none; background-color: rgb(255,51); word-break: break-all; word-spacing: 0px; padding-top: 0px; -webkit-text-stroke-width: 0px">那,以下就是使用线程池来实现的代理,按道理讲,退出时池要shutdown(),监听socket也要shutdown的,但百行代码,就不勉强了,随着ctrl + c,就让操作系统来回收资源吧。
<p style="text-align: left; padding-bottom: 0px; widows: 1; text-transform: none; background-color: rgb(255,51); word-break: break-all; word-spacing: 0px; padding-top: 0px; -webkit-text-stroke-width: 0px">PHP不擅长网络编程体现在哪里呢?首先我用的是stream_socket_XXX相关的函数,为啥不用socket扩展呢?因为socket扩展有问题,参见:<a style="padding-bottom: 0px; margin: 0px; outline-style: none; outline-color: invert; padding-left: 0px; outline-width: 0px; padding-right: 0px; color: rgb(255,115); font-size: 12px; padding-top: 0px" href="https://github.com/krakjoe/pthreads/issues/581">https://github.com/krakjoe/pthreads/issues/581<span class="Apple-converted-space">而stream_set_timeout对stream_socket_recvfrom这些高级操作,不起作用,参见:<a style="padding-bottom: 0px; margin: 0px; outline-style: none; outline-color: invert; padding-left: 0px; outline-width: 0px; padding-right: 0px; color: rgb(255,115); font-size: 12px; padding-top: 0px" href="http://php.net/manual/en/function.stream-set-timeout.php">http://php.net/manual/en/function.stream-set-timeout.php<span class="Apple-converted-space">而这些,在写代理时都需要考虑的。比如连接远程目标服务器时,没有超时控制,很容易就线程池跑满了。
<p style="text-align: left; padding-bottom: 0px; widows: 1; text-transform: none; background-color: rgb(255,51); word-break: break-all; word-spacing: 0px; padding-top: 0px; -webkit-text-stroke-width: 0px">测试的话,使用curl即可,对了,目前只支持远程dns解析,为啥呢?因为这个玩具后期可是要实现禾斗学上网的哟: curl --socks5-hostname 127.0.0.1:1080<span class="Apple-converted-space"><a style="padding-bottom: 0px; margin: 0px; outline-style: none; outline-color: invert; padding-left: 0px; outline-width: 0px; padding-right: 0px; color: rgb(255,115); font-size: 12px; padding-top: 0px" href="http://ip.cn/">http://ip.cn
<div class="jb51code">
<pre class="brush:php;">
Class Pipe extends Threaded
{
private $client;
private $remote;
public function __construct($client,$remote)
{
$this->client = $client;
$this->remote = $remote;
}
public function run()
{
for ( ; ; ) {
$data = stream_socket_recvfrom($this->client,4096);
if ($data === false || strlen($data) === 0) {
break;
}
$sendBytes = stream_socket_sendto($this->remote,$data);
if ($sendBytes <= 0) {
break;
}
}
stream_socket_shutdown($this->client,STREAM_SHUT_RD);
stream_socket_shutdown($this->remote,STREAM_SHUT_WR);
}
}
Class Client extends Threaded
{
public $fd;
public function __construct($fd)
{
$this->fd = $fd;
}
public function run()
{
$data = stream_socket_recvfrom($this->fd,2);
$data = unpack('c',$data);
if ($data[1] !== 0x05) {
stream_socket_shutdown($this->fd,STREAM_SHUT_RDWR);
echo '协议不正确.',PHP_EOL;
return;
}
$nmethods = $data[2];
$data = stream_socket_recvfrom($this->fd,$nmethods);
stream_socket_sendto($this->fd,"\x05\x00");
$data = stream_socket_recvfrom($this->fd,4);
$data = unpack('c',$data);
$addressType = $data[4];
if ($addressType === 0x03) { // domain
$domainLength = unpack('c',stream_socket_recvfrom($this->fd,1))[1];
$data = stream_socket_recvfrom($this->fd,$domainLength + 2);
$domain = substr($data,$domainLength);
$port = unpack("n",substr($data,-2))[1];
} else {
stream_socket_shutdown($this->fd,STREAM_SHUT_RDWR);
echo '请使用远程dns解析.',PHP_EOL;
}
stream_socket_sendto($this->fd,"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00");
echo "{$domain}:{$port}",<a href="/tag/PHP/" target="_blank" class="keywords">PHP</a>_EOL;
$remote = stream_socket_client("tcp://{$domain}:{$port}");
if ($remote === false) {
stream_socket_shutdown($this->fd,STREAM_SHUT_RDWR);
return;
}
$pool = $this->worker->pipePool;
$pipe1 = new Pipe($remote,$this->fd);
$pipe2 = new Pipe($this->fd,$remote);
$pool->submit($pipe1);
$pool->submit($pipe2);
}
}
class ProxyWorker extends Worker
{
public $pipePool;
public function __construct($pipePool)
{
$this->pipePool = $pipePool;
}
}
$server = stream_socket_server('tcp://0.0.0.0:1080',$errno,$errstr);
if ($server === false)
exit($errstr);
$pipePool = new Pool(200,Worker::class);
$pool = new Pool(50,'ProxyWorker',[$pipePool]);
for( ; ; ) {
$fd = @stream_socket_accept($server,60);
if ($fd === false)
continue;
$pool->submit(new Client($fd));
}