100行PHP代码实现socks5代理服务器


Posted in PHP onApril 28, 2016

前两天在B站上看到一个小伙纸100元组装个电脑打LOL画质流畅,突发奇想100行代码能(简单)实现个啥好玩的。我主要是做php开发的,于是就有了本文。

当然,由于php(不算swoole扩展)本身不擅长做网络服务端编程,所以这个代理,只是个玩具,离日常使用有点距离。如果想使用稳定可靠的加密(所以能禾斗学上网)代理,可以用这个:https://github.com/momaer/asocks-go也是100来行代码使用go实现。

写的过程中发现php多线程还是难的。比如我开始想每个连接新建一个线程。但这个线程得保存起来(比如保存到数组),比如官方例子中的这个:https://github.com/krakjoe/pthreads/blob/master/examples/SocketServer.php 要放到$clients这个数组里,不然,你试试(curl -L一个要301的地址)就知道出现什么情况了。

这个例子说了in the real world, do something here to ensure clients not running are destroyed 但是,如何把不再运行的连接销毁却没有讲。恩。我试了把$clients放到一个类里,把类传给线程类,然后在线程类要结束时把$clients里对应的连接给unset掉,无果。

那,以下就是使用线程池来实现的代理,按道理讲,退出时池要shutdown(),监听socket也要shutdown的,但百行代码,就不勉强了,随着ctrl + c,就让操作系统来回收资源吧。

php不擅长网络编程体现在哪里呢?首先我用的是stream_socket_XXX相关的函数,为啥不用socket扩展呢?因为socket扩展有问题,参见:https://github.com/krakjoe/pthreads/issues/581 而stream_set_timeout对stream_socket_recvfrom这些高级操作,不起作用,参见:http://php.net/manual/en/function.stream-set-timeout.php 而这些,在写代理时都需要考虑的。比如连接远程目标服务器时,没有超时控制,很容易就线程池跑满了。

测试的话,使用curl即可,对了,目前只支持远程dns解析,为啥呢?因为这个玩具后期可是要实现禾斗学上网的哟: curl --socks5-hostname 127.0.0.1:1080 http://ip.cn

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, 0, $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}", PHP_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));
}
PHP 相关文章推荐
在PHP3中实现SESSION的功能(三)
Oct 09 PHP
详解php的魔术方法__get()和__set()使用介绍
Sep 19 PHP
ThinkPHP实例化模型的四种方法概述
Aug 22 PHP
PHP判断一个gif图片是否为动态图片的方法
Nov 19 PHP
PHP获取数组的键与值方法小结
Jun 13 PHP
Yii2.0 Basic代码中路由链接被转义的处理方法
Sep 21 PHP
PHP用mysql_insert_id()函数获得刚插入数据或当前发布文章的ID
Nov 25 PHP
PHP实现截取中文字符串不出现?号的解决方法
Dec 29 PHP
php实现评论回复删除功能
May 23 PHP
关于ThinkPhp 框架表单验证及ajax验证问题
Jul 19 PHP
PHP生成随机数的方法总结
Mar 01 PHP
thinkPHP框架实现生成条形码的方法示例
Jun 06 PHP
Yii2实现ajax上传图片插件用法
Apr 28 #PHP
thinkphp3.2实现上传图片的控制器方法
Apr 28 #PHP
PHP简单实现文本计数器的方法
Apr 28 #PHP
Yii2 rbac权限控制之菜单menu实例教程
Apr 28 #PHP
Yii2搭建后台并实现rbac权限控制完整实例教程
Apr 28 #PHP
PHP在线调试执行的实现方法(附demo源码)
Apr 28 #PHP
thinkphp项目部署到Linux服务器上报错“模板不存在”如何解决
Apr 27 #PHP
You might like
PHP防注入安全代码
2008/04/09 PHP
linux中cd命令使用详解
2015/01/08 PHP
PHP中imagick函数的中文解释
2015/01/21 PHP
浅谈PHP表单提交(POST&amp;GET&amp;URL编/解码)
2017/04/03 PHP
国外Lightbox v2.03.3 最新版 下载
2007/10/17 Javascript
js parseInt(&quot;08&quot;)未指定进位制问题
2010/06/19 Javascript
javascript获取下拉列表框当中的文本值示例代码
2013/07/31 Javascript
js和jquery中循环的退出和继续学习记录
2014/09/06 Javascript
jQuery选择器全集详解
2014/11/24 Javascript
在JavaScript的正则表达式中使用exec()方法
2015/06/16 Javascript
jQuery取得iframe中元素的常用方法详解
2016/01/14 Javascript
Node.js 条形码识别程序构建思路详解
2016/02/14 Javascript
JavaScript里 ==与===区别详解
2016/08/16 Javascript
Angular路由简单学习
2016/12/26 Javascript
js/jq仿window文件夹移动/剪切/复制等操作代码
2017/03/08 Javascript
vue 组件内获取actions的response方式
2019/11/08 Javascript
浅谈关于vue中scss公用的解决方案
2019/12/02 Javascript
vue项目中使用vue-layer弹框插件的方法
2020/03/11 Javascript
Python使用MySQLdb for Python操作数据库教程
2014/10/11 Python
PyCharm在win10的64位系统安装实例
2017/11/26 Python
python requests 测试代理ip是否生效
2018/07/25 Python
Linux下多个Python版本安装教程
2018/08/15 Python
PyCharm设置Ipython交互环境和宏快捷键进行数据分析图文详解
2020/04/23 Python
python实现人像动漫化的示例代码
2020/05/17 Python
大数据分析用java还是Python
2020/07/06 Python
Python安装并操作redis实现流程详解
2020/10/13 Python
英国领先的珍珠首饰品牌:Orchira
2016/09/11 全球购物
法国美发器材和产品购物网站:Beauty Coiffure
2016/12/05 全球购物
物业管理计划书
2014/01/10 职场文书
毕业自我评价
2014/02/05 职场文书
金融学专业大学生职业生涯规划
2014/03/07 职场文书
幼儿教师培训感言
2014/03/08 职场文书
汶川大地震感悟
2015/08/10 职场文书
军训决心书范文
2015/09/22 职场文书
小学学习委员竞选稿
2015/11/20 职场文书
mysql实现将字符串字段转为数字排序或比大小
2022/06/14 MySQL