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 相关文章推荐
一个分页的论坛
Oct 09 PHP
PHP中for循环语句的几种变型
Nov 26 PHP
PHP date函数参数详解
Nov 27 PHP
php AJAX实例根据邮编自动完成地址信息
Nov 23 PHP
php中存储用户ID和密码到mysql数据库的方法
Feb 06 PHP
解析PHP强制转换类型及远程管理插件的安全隐患
Jun 30 PHP
自己写的php中文截取函数mb_strlen和mb_substr
Feb 09 PHP
php文件上传类完整实例
May 14 PHP
深入剖析PHP中printf()函数格式化使用
May 23 PHP
php opendir()列出目录下所有文件的实例代码
Oct 02 PHP
PHP get_html_translation_table()函数用法讲解
Feb 16 PHP
浅谈PHP进程管理
Mar 08 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
基于qmail的完整WEBMAIL解决方案安装详解
2006/10/09 PHP
PHP保留两位小数并且四舍五入及不四舍五入的方法
2013/09/22 PHP
在Yii框架中使用PHP模板引擎Twig的例子
2014/06/13 PHP
分析PHP中单双引号的误区和双引号小隐患
2016/07/19 PHP
PHP实现微信提现(企业付款到零钱)
2019/08/01 PHP
JS window对象的top、parent、opener含义介绍
2013/12/03 Javascript
通过pjax实现无刷新翻页(兼容新版jquery)
2014/01/31 Javascript
jquery判断元素是否隐藏的多种方法
2014/05/06 Javascript
js style动态设置table高度
2014/10/21 Javascript
JavaScript中的ArrayBuffer详细介绍
2014/12/08 Javascript
javascript运动详解
2015/07/06 Javascript
JavaScript实现添加及删除事件的方法小结
2015/08/04 Javascript
JS实现的5级联动Select下拉选择框实例
2015/08/17 Javascript
jQuery左侧大图右侧小图焦点图幻灯切换代码分享
2015/08/19 Javascript
解析js如何获取css样式
2016/12/11 Javascript
JS实现直接运行html代码的方法
2017/03/13 Javascript
通过js修改input、select默认字体颜色
2017/04/19 Javascript
vue项目打包为APP,静态资源正常显示,但API请求不到数据的操作
2020/09/12 Javascript
原生js 实现表单验证功能
2021/02/08 Javascript
windows系统中python使用rar命令压缩多个文件夹示例
2014/05/06 Python
Python的装饰器模式与面向切面编程详解
2015/06/21 Python
Python实现爬取需要登录的网站完整示例
2017/08/19 Python
Python cookbook(数据结构与算法)实现优先级队列的方法示例
2018/02/18 Python
pygame游戏之旅 游戏中添加显示文字
2018/11/20 Python
Python3爬虫爬取百姓网列表并保存为json功能示例【基于request、lxml和json模块】
2018/12/05 Python
对Python 简单串口收发GUI界面的实例详解
2019/06/12 Python
Python搭建Keras CNN模型破解网站验证码的实现
2020/04/07 Python
django下创建多个app并设置urls方法
2020/08/02 Python
Canvas在超级玛丽游戏中的应用详解
2021/02/06 HTML / CSS
Java面试题及答案
2012/09/08 面试题
大唐面试试题(CPU,UNIX等等)
2012/01/11 面试题
动物科学专业毕业生的自我评价
2013/11/29 职场文书
大学生村官个人总结
2015/02/15 职场文书
基于Python实现的购物商城管理系统
2021/04/27 Python
nginx部署多前端项目的几种方法
2021/05/25 Servers
Redis实战之Lettuce的使用技巧详解
2022/12/24 Redis