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 相关文章推荐
解析ajax事件的调用顺序
Jun 17 PHP
CodeIgniter启用缓存和清除缓存的方法
Jun 12 PHP
PHP中使用GD库绘制折线图 折线统计图的绘制方法
Nov 09 PHP
学习php设计模式 php实现合成模式(composite)
Dec 08 PHP
汇总PHPmailer群发Gmail的常见问题
Feb 24 PHP
php中namespace及use用法分析
Dec 06 PHP
thinkPHP中session()方法用法详解
Dec 08 PHP
PHP自动补全表单的两种方法
Mar 06 PHP
php实现生成带二维码图片并强制下载功能
Feb 24 PHP
windows环境下使用Composer安装ThinkPHP5
May 18 PHP
PHP观察者模式示例【Laravel框架中有用到】
Jun 15 PHP
PHP工厂模式的日常使用
Mar 20 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来实现网络服务
2009/09/15 PHP
php获取从百度、谷歌等搜索引擎进入网站关键词的方法
2015/07/08 PHP
thinkPHP利用ajax异步上传图片并显示、删除的示例
2018/09/26 PHP
thinkPHP3.2使用RBAC实现权限管理的实现
2019/08/27 PHP
获取dom元素那些讨厌的位置封装代码
2010/06/23 Javascript
js 取时间差去掉周六周日实现代码
2012/12/25 Javascript
基于jquery实现的定时显示与隐藏div广告的实现代码
2013/08/22 Javascript
node爬取微博的数据的简单封装库nodeweibo使用指南
2015/01/02 Javascript
Node.js实用代码段之正确拼接Buffer
2016/03/17 Javascript
JS控制伪元素的方法汇总
2016/04/06 Javascript
Ext JS 实现建议词模糊动态搜索功能
2017/05/13 Javascript
详解vue移动端日期选择组件
2018/02/22 Javascript
Vue $mount实战之实现消息弹窗组件
2019/04/22 Javascript
VUE项目初建和常见问题总结
2019/09/12 Javascript
[01:23]一分钟告诉你 DOTA2为什么叫信仰2
2014/06/20 DOTA
[08:53]DOTA2-DPC中国联赛 正赛 PSG.LGD vs LBZS 选手采访
2021/03/11 DOTA
Sublime开发python程序的示例代码
2018/01/24 Python
pandas对指定列进行填充的方法
2018/04/11 Python
对Python正则匹配IP、Url、Mail的方法详解
2018/12/25 Python
Django Sitemap 站点地图的实现方法
2019/04/29 Python
python通过robert、sobel、Laplace算子实现图像边缘提取详解
2019/08/21 Python
python飞机大战pygame游戏框架搭建操作详解
2019/12/17 Python
解决Python数据可视化中文部分显示方块问题
2020/05/16 Python
python实现马丁策略回测3000只股票的实例代码
2021/01/22 Python
自荐信怎么写好
2013/11/11 职场文书
医药工作岗位求职信分享
2013/12/31 职场文书
KTV的创业计划书范文
2014/02/02 职场文书
小学语文教学反思
2014/02/10 职场文书
初二学习计划书范文
2014/04/27 职场文书
幼儿园优秀班主任事迹材料
2014/05/14 职场文书
国际商贸专业自荐信
2014/06/09 职场文书
党的群众路线教育实践活动个人整改措施落实情况
2014/11/04 职场文书
2014年扶贫工作总结
2014/11/18 职场文书
求职自我评价参考范文
2019/05/16 职场文书
Python词云的正确实现方法实例
2021/05/08 Python
springboot如何初始化执行sql语句
2021/06/22 Java/Android