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里的JS打印函数
Oct 09 PHP
把PHP安装为Apache DSO
Oct 09 PHP
提取HTML标签
Oct 09 PHP
如何写php程序?
Dec 08 PHP
php xml 入门学习资料
Jan 01 PHP
yii框架配置默认controller和action示例
Apr 30 PHP
php使用pack处理二进制文件的方法
Jul 03 PHP
php+mysqli使用面向对象方式查询数据库实例
Jan 29 PHP
SESSION存放在数据库用法实例
Aug 08 PHP
Windows下PHP开发环境搭建教程(Apache+PHP+MySQL)
Jun 13 PHP
PHP HTTP 认证实例详解
Nov 03 PHP
PDO::lastInsertId讲解
Jan 29 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
destoon官方标签大全
2014/06/20 PHP
PHP页面间传递值和保持值的方法
2016/08/24 PHP
javascript iframe编程相关代码
2009/12/28 Javascript
基于jquery实现的类似百度搜索的输入框自动完成功能
2011/08/23 Javascript
JS预览图像将本地图片显示到浏览器上
2013/08/25 Javascript
json数据与字符串的相互转化示例
2013/09/18 Javascript
jquery(hide方法)隐藏指定元素实例
2013/11/11 Javascript
不使用jquery实现js打字效果示例分享
2014/01/19 Javascript
javascript自定义函数参数传递为字符串格式
2014/07/29 Javascript
js实现键盘上下左右键选择文字并显示在文本框的方法
2015/05/07 Javascript
JavaScript实现网页加载进度条代码超简单
2015/09/21 Javascript
Sort()函数的多种用法
2016/03/20 Javascript
深入浅析AngularJS中的一次性数据绑定 (bindonce)
2017/05/11 Javascript
JavaScript之json_动力节点Java学院整理
2017/06/29 Javascript
Angularjs的键盘事件的绑定
2017/07/27 Javascript
基于Bootstrap框架菜鸟入门教程(推荐)
2017/09/17 Javascript
ajax与jsonp的区别及用法
2018/10/16 Javascript
详解nodejs解压版安装和配置(带有搭建前端项目脚手架)
2018/12/06 NodeJs
vue的三种图片引入方式代码实例
2019/11/19 Javascript
仅用500行Python代码实现一个英文解析器的教程
2015/04/02 Python
Python部署web开发程序的几种方法
2017/05/05 Python
基于python的多进程共享变量正确打开方式
2018/04/28 Python
flask入门之文件上传与邮件发送示例
2018/07/18 Python
​如何愉快地迁移到 Python 3
2019/04/28 Python
python pygame实现方向键控制小球
2019/05/17 Python
Python button选取本地图片并显示的实例
2019/06/13 Python
python实现ip地址查询经纬度定位详解
2019/08/30 Python
Python连接SQLite数据库并进行增册改查操作方法详解
2020/02/18 Python
python使用html2text库实现从HTML转markdown的方法详解
2020/02/21 Python
python redis存入字典序列化存储教程
2020/07/16 Python
使用phonegap进行提示操作的具体方法
2017/03/30 HTML / CSS
大学四年职业生涯规划书范文
2014/01/02 职场文书
幼儿园亲子活动方案
2014/01/29 职场文书
党员干部承诺书
2014/03/25 职场文书
党员公开承诺书内容
2014/05/20 职场文书
python模拟浏览器 使用selenium进入好友QQ空间并留言
2022/04/12 Python