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 相关文章推荐
用Flash图形化数据(二)
Oct 09 PHP
PHP 中的面向对象编程:通向大型 PHP 工程的办法
Dec 03 PHP
PHP开发的一些注意点总结
Oct 12 PHP
php自定义session示例分享
Apr 22 PHP
php字符串截取函数用法分析
Nov 25 PHP
php将csv文件导入到mysql数据库的方法
Dec 24 PHP
php获取json数据所有的节点路径
May 17 PHP
PHP设计模式之观察者模式实例
Feb 22 PHP
详解在PHP的Yii框架中使用行为Behaviors的方法
Mar 18 PHP
Yii2实现增删改查后留在当前页的方法详解
Jan 13 PHP
Laravel给生产环境添加监听事件(SQL日志监听)
Jun 19 PHP
laravel实现简单用户权限的示例代码
May 28 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
Yii中CGridView关联表搜索排序方法实例详解
2014/12/03 PHP
php获取POST数据的三种方法实例详解
2016/12/20 PHP
javascript实现动态增加删除表格行(兼容IE/FF)
2007/04/02 Javascript
JQuery实现简单时尚快捷的气泡提示插件
2012/12/20 Javascript
ajax与302响应代码测试
2013/10/23 Javascript
javascript面向对象之定义成员方法实例分析
2015/01/13 Javascript
js实现非常简单的焦点图切换特效实例
2015/05/07 Javascript
浅谈bootstrap源码分析之tab(选项卡)
2016/06/06 Javascript
BootStrap Typeahead自动补全插件实例代码
2016/08/10 Javascript
Angular2从搭建环境到开发步骤详解
2016/10/17 Javascript
老生常谈js中0到底是 true 还是 false
2017/03/08 Javascript
使用Node.js实现RESTful API的示例
2017/08/01 Javascript
浅谈Vue.nextTick 的实现方法
2017/10/25 Javascript
使用vue官方提供的模板vue-cli搭建一个helloWorld案例分析
2018/01/16 Javascript
解决淘宝cnpm 安装后cnpm不是内部或外部命令的问题
2018/05/17 Javascript
详解JavaScript 浮点数运算的精度问题
2019/07/23 Javascript
vue+elementui实现点击table中的单元格触发事件--弹框
2020/07/18 Javascript
vue-cli —— 如何局部修改Element样式
2020/10/22 Javascript
[04:52]第二届DOTA2亚洲邀请赛主赛事第一天比赛集锦:OG娜迦海妖放大配合谜团大中3人
2017/04/02 DOTA
python列表操作实例
2015/01/14 Python
Django中模型Model添加JSON类型字段的方法
2015/06/17 Python
Python中map,reduce,filter和sorted函数的使用方法
2015/08/17 Python
Django中使用celery完成异步任务的示例代码
2018/01/23 Python
python3实现磁盘空间监控
2018/06/21 Python
python 实现绘制整齐的表格
2019/11/18 Python
在Keras中CNN联合LSTM进行分类实例
2020/06/29 Python
高性能钓鱼服装:Huk Gear
2019/02/20 全球购物
如果重写了对象的equals()方法,需要考虑什么
2014/11/02 面试题
捐款倡议书格式范文
2014/05/14 职场文书
政风行风整改报告
2014/11/06 职场文书
2015年信访工作总结
2015/04/07 职场文书
欧也妮葛朗台读书笔记
2015/06/30 职场文书
信息技术国培研修日志
2015/11/13 职场文书
小学班级标语口号大全
2015/12/26 职场文书
参观监狱警示教育心得体会
2016/01/15 职场文书
Go归并排序算法的实现方法
2022/04/06 Golang