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 相关文章推荐
PHP在Web开发领域的优势
Oct 09 PHP
PHP调用Twitter的RSS的实现代码
Mar 10 PHP
php时间戳转换的示例
Mar 31 PHP
php中$美元符号与Zen Coding冲突问题解决方法分享
May 28 PHP
php转换颜色为其反色的方法
Apr 27 PHP
ThinkPHP中使用Ueditor富文本编辑器
Sep 02 PHP
PHP中的随机性 你觉得自己幸运吗?
Jan 22 PHP
Linux(CentOS)下PHP扩展PDO编译安装的方法
Apr 07 PHP
laravel如何开启跨域功能示例详解
Aug 31 PHP
Thinkphp 5.0实现微信企业付款到零钱
Sep 30 PHP
phpinfo无法显示的原因及解决办法
Feb 15 PHP
php模拟post提交请求调用接口示例解析
Aug 07 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使用正则表达式清除超链接文本
2013/11/12 PHP
Codeigniter中mkdir创建目录遇到权限问题和解决方法
2014/07/25 PHP
Thinkphp 中 distinct 的用法解析
2016/12/14 PHP
基于swoole实现多人聊天室
2018/06/14 PHP
PHP+Apache实现二级域名之间共享cookie的方法
2019/07/24 PHP
js 验证密码强弱的小例子
2013/03/21 Javascript
jquery动态增加text元素以及删除文本内容实例代码
2013/07/01 Javascript
js 获取input点选按钮的值的方法
2014/04/14 Javascript
jquery实现人性化的有选择性禁用鼠标右键
2014/06/30 Javascript
js实现同一页面可多次调用的图片幻灯切换效果
2015/02/28 Javascript
在Node.js应用中读写Redis数据库的简单方法
2015/06/30 Javascript
详解Javascript模板引擎mustache.js
2016/01/20 Javascript
微信小程序 火车票查询实例讲解
2016/10/17 Javascript
PHP捕捉异常中断的方法
2016/10/24 Javascript
微信小程序 简单教程实例详解
2017/01/13 Javascript
网页中右键功能的实现方法之contextMenu的使用
2017/02/20 Javascript
通过js动态创建标签,并设置属性方法
2018/02/24 Javascript
对angular2中的ngfor和ngif指令嵌套实例讲解
2018/09/12 Javascript
实例分析Array.from(arr)与[...arr]到底有何不同
2019/04/09 Javascript
详解JS实现简单的时分秒倒计时代码
2019/04/25 Javascript
Node.js 的 GC 机制详解
2019/06/03 Javascript
js 动态校验开始结束时间的实现代码
2020/05/25 Javascript
[03:12]TI9战队档案 - Virtus Pro
2019/08/20 DOTA
跟老齐学Python之重回函数
2014/10/10 Python
Flask框架学习笔记之路由和反向路由详解【图文与实例】
2019/08/12 Python
python 数据提取及拆分的实现代码
2019/08/26 Python
原生python实现knn分类算法
2019/10/24 Python
Python常用外部指令执行代码实例
2020/11/05 Python
IE8下CSS3选择器nth-child() 不兼容问题的解决方法
2016/11/16 HTML / CSS
全球领先的在线cosplay服装商店:RoleCosplay
2020/01/18 全球购物
服务中心夜班服务员岗位职责
2013/11/27 职场文书
茶叶店创业计划书范文
2014/01/19 职场文书
班级口号大全
2014/06/09 职场文书
教师节获奖感言
2015/07/31 职场文书
中秋晚会致辞
2015/07/31 职场文书
朋友聚会祝酒词
2015/08/10 职场文书