100多行PHP代码实现socks5代理服务器,这次是使用swoole纯异步来写,使用状态机来处理数据。目前用它访问开源中国木有压力,但访问网易新闻就压力山大。我发现我用别的语言写得代理,访问网易新闻都压力大。嘎嘎,学艺不精。

对swoole理解不深,不知道怎么处理socket shutdown只关闭读/写这样,还有就是连接超时,读写超时这种怎么处理。在网上看到作者说要用定时器,感觉好麻烦,所以,这次的代理,虽然个人用,一般不会有什么问题,但离产品级的代理,还有段路要走。

如果要利用多核,就使用process模式,设置worker个数为cpu数量即可。

<?php
class Client
{
 public $connected = true;
 public $data = "";
 public $remote = null;
 public $status = 0;
}
class Server
{
 public $clients = [];
 public function start()
 {
  $server = new swoole_server("0.0.0.0", 8388, SWOOLE_BASE, SWOOLE_SOCK_TCP);
  $server->set([
   "max_conn" => 1000, 
   "daemonize" => 1,
   "reactor_num" => 1,
   "worker_num" => 1,
   "dispatch_mode" => 2,
   "buffer_output_size" => 128 * 1024 * 1024,
   "open_cpu_affinity" => 1,
   "open_tcp_nodelay" => 1,
   "log_file" => "socks5_server.log",
  ]);
  $server->on("connect", [$this, "onConnect"]);
  $server->on("receive", [$this, "onReceive"]);
  $server->on("close", [$this, "onClose"]);
  $server->start();
 }
 public function onConnect($server, $fd, $fromID)
 {
  $this->clients[$fd] = new Client();
 }
 public function onReceive($server, $fd, $fromID, $data)
 {
  ($this->clients[$fd])->data .= $data;
  $this->parse($server, $fd); 
 }
 public function onClose($server, $fd, $fromID)
 {
  $client = $this->clients[$fd];
  $client->connected = false;
 }
 private function parse($server, $fd) 
 {
  $client = $this->clients[$fd];

  switch ($client->status) {
   case 0: {
    if (strlen($client->data) >= 2) {
     $request = unpack("c*", substr($client->data, 0, 2));
     if ($request[1] !== 0x05) {
      echo "协议不正确:" . $request[1], PHP_EOL;
      $server->close($fd);
      break;
     }
     $nmethods = $request[2];
     if (strlen($client->data) >= 2 + $nmethods) {
      $client->data = substr($client->data, 2 + $nmethods);
      $server->send($fd, "x05x00");
      $client->status = 1;
     }
    }
   }
   case 1: {
    if (strlen($client->data) < 5)
     break;
    $request = unpack("c*", $client->data);
    $aType = $request[4];
    if ($aType === 0x03) { // domain
     $domainLen = $request[5];
     if (strlen($client->data) < 5 + $domainLen + 2) { 
      break; 
     }
     $domain = substr($client->data, 5, $domainLen);
     $port = unpack("n", substr($client->data, 5 + $domainLen, 2))[1]; 
     $client->data = substr($client->data, 5 + $domainLen + 2);
    } else if ($aType === 0x01) { // ipv4
     $domain = long2ip(unpack("N", substr($client->data, 4, 4))[1]);
     $port = unpack("n", substr($client->data, 8, 2))[1]; 
     $client->data = substr($client->data, 10);
    } else {
     echo "不支持的atype:" . $aType, PHP_EOL;
     $server->close($fd);
     break;
    }

    $remote = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
    $remote->on("connect", function($cli) use($client, $server, $fd, $remote) {
     $server->send($fd, "x05x00x00x01x00x00x00x00x00x00");
     $client->status = 2;
     $client->remote = $remote;
    });
    $remote->on("error", function(swoole_client $cli) use($server, $fd) {
     //$server->send($fd, ""); // todo 连接不上remote
     echo "connect to remote error.", PHP_EOL;
     $server->close($fd);
    });
    $remote->on("receive", function($cli, $data) use($server, $fd, $client) {
     if (!$client->connected) {
      echo "connection has been closed.", PHP_EOL;
      return;
     }
     $server->send($fd, $data);
    });
    $remote->on("close", function($cli) use($server, $fd, $client) {
     $client->remote = null;
    });
    if ($aType === 0x03) {
     swoole_async_dns_lookup($domain, function($host, $ip) use($remote, $port, $server, $fd) {
      //todo 当host为空时的处理。貌似不存在的域名都解析成了本机的外网ip,奇怪
      if (empty($ip) || empty($host)) {
       echo "host:{$host}, ip:{$ip}
";
       $server->close($fd);
       return;
      }
      $remote->connect($ip, $port);
     });
    } else {
     $remote->connect($domain, $port);
    }
   }
   case 2: {
    if (strlen($client->data) === 0) {
     break;
    }
    if ($client->remote === null) {
     echo "remote connection has been closed.", PHP_EOL;
     break;
    }

    $sendByteCount = $client->remote->send($client->data);
    if ($sendByteCount === false || $sendByteCount < strlen($client->data)) {
     echo "data length:" , strlen($client->data), " send byte count:", $sendByteCount, PHP_EOL; 
     echo $client->data, PHP_EOL;
     $server->close($fd); 
    }
    $client->data = "";
   }
  }
 }
}

(new Server())->start();