100行PHP代码实现socks5代理服务器

5年以前  |  阅读数:545 次  |  编程语言:PHP 

前两天在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分页显示制作详细讲解
SSH 登录失败:Host key verification failed
获取IMSI
将二进制数据转为16进制以便显示
获取IMEI
文件下载
贪吃蛇
双位运算符
PHP自定义函数获取搜索引擎来源关键字的方法
Java生成UUID
发送邮件
年的日历图
提取后缀名
在Zeus Web Server中安装PHP语言支持
让你成为最历害的git提交人
Yii2汉字转拼音类的实例代码
再谈PHP中单双引号的区别详解
指定应用ID以获取对应的应用名称
Python 2与Python 3版本和编码的对比
php封装的page分页类完整实例