基于 workerman 搭建内网穿透服务 windows 环境


背景:

项目需要访问内网的数据库,数据库又不能直接访问外网。而且外网IP还随时在变化(大概5分钟就会变化一次)。

方案:

使用中间机做转发。整个转发基于 workerman 实现。

下面是代码:

composer.json

{
    "name"  : "transmit-demo",
    "keywords": ["distributed","communication"],
    "homepage": "http://www.workerman.net",
    "license" : "MIT",
    "require": {
        "workerman/gateway-worker-for-win" : ">=3.0.0",
        "workerman/channel": "^1.0"
    }
}

Server 端 Channel

<?php

use Workerman\Worker;

// 自动加载类
require_once __DIR__ . '/../../vendor/autoload.php';

$channel_server = new Channel\Server('0.0.0.0', 12345);

// 如果不是在根目录启动,则运行runAll方法
if (!defined('GLOBAL_START')) {
  Worker::runAll();
}

Server 端 Server

<?php

use Workerman\Lib\Timer;
use \Workerman\Worker;

// 自动加载类
require_once __DIR__ . '/../../vendor/autoload.php';

$config = [
  'server_port' => '54321',
  'channel_port' => '12345',
  'channel_unique_key' => '',
];

$outside_worker = new Worker('tcp://0.0.0.0:' . $config['server_port']);

$outside_worker->isClientOnline = FALSE;

$outside_worker->lastRegisterTime = time();

$outside_worker->onWorkerStart = function () use ($outside_worker, $config) {
  // Channel客户端连接到Channel服务端
  Channel\Client::connect('127.0.0.1', $config['channel_port']);

  Timer::add(1, function () use ($outside_worker) {
    if (time() - $outside_worker->lastRegisterTime > 5) {
      $outside_worker->isClientOnline = FALSE;
      foreach ($outside_worker->connections as $c_key => $c_value) {
        $c_value->close();
      }
    }
  });


  Channel\Client::on('cs_register', function ($event_data) use ($config, $outside_worker) {
    if ($event_data['channel_unique_key'] == $config['channel_unique_key']) {
      $outside_worker->isClientOnline = TRUE;
      $outside_worker->lastRegisterTime = time();
      $register_data['status'] = 1;
      Channel\Client::publish('sc_register_' . $config['channel_unique_key'], $register_data);
    }
  });

  Channel\Client::on('sc_message_' . $config['channel_unique_key'], function ($event_data) use ($outside_worker) {
    if (isset($outside_worker->connections[$event_data['connection']['c_connection_id']])) {
      $outside_worker->connections[$event_data['connection']['c_connection_id']]->send($event_data['data']);
    }
  });

  Channel\Client::on('sc_close_' . $config['channel_unique_key'], function ($event_data) use ($outside_worker) {
    if (isset($outside_worker->connections[$event_data['connection']['c_connection_id']])) {
      $outside_worker->connections[$event_data['connection']['c_connection_id']]->close();
    }
  });
  Channel\Client::on('sc_connect_' . $config['channel_unique_key'], function ($event_data) use ($outside_worker) {
  });

};

$outside_worker->onConnect = function ($connection) use ($config, $outside_worker) {
  if (!$outside_worker->isClientOnline) {
    $connection->close();
  }
  $connection_data['connection'] = [
    'ip' => $connection->getRemoteIp(),
    'port' => $connection->getRemotePort(),
    'c_connection_id' => $connection->id,
  ];
  Channel\Client::publish('cs_connect_' . $config['channel_unique_key'], $connection_data);
  $connection->onMessage = function ($connection, $data) use ($config) {
    $message_data['connection'] = [
      'ip' => $connection->getRemoteIp(),
      'port' => $connection->getRemotePort(),
      'c_connection_id' => $connection->id,
    ];
    $message_data['data'] = $data;
    Channel\Client::publish('cs_message_' . $config['channel_unique_key'], $message_data);
  };
  $connection->onClose = function ($connection) use ($config) {
    $close_data['connection'] = [
      'ip' => $connection->getRemoteIp(),
      'port' => $connection->getRemotePort(),
      'c_connection_id' => $connection->id,
    ];
    Channel\Client::publish('cs_close_' . $config['channel_unique_key'], $close_data);
  };
};
// 如果不是在根目录启动,则运行runAll方法
if (!defined('GLOBAL_START')) {
  Worker::runAll();
}

Client 端(中转机)

<?php
/**
 * This file is part of workerman.
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the MIT-LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @author walkor<walkor@workerman.net>
 * @copyright walkor<walkor@workerman.net>
 * @link http://www.workerman.net/
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 */
use Workerman\Connection\AsyncTcpConnection;
use Workerman\Lib\Timer;
use \Workerman\Worker;

require_once __DIR__ . '/../../vendor/autoload.php';

$config = [
  'server_ip' => '',
  'channel_port' => '12345',
  'channel_unique_key' => '',
  'local_ip' => '192.168.1.2',
  'local_port' => '3389',
];

$inside_worker = new Worker();

$inside_worker->inChannel = FALSE;

$inside_worker->onWorkerStart = function () use ($inside_worker, $config) {

  // Channel客户端连接到Channel服务端
  Channel\Client::connect($config['server_ip'], $config['channel_port']);

  //注册客户端到分布式通讯服务

  // 2.5秒执行一次
  $time_interval = 2.5;
  $inside_worker->start_register_timer = Timer::add($time_interval, function () use ($config) {
    $register_data['channel_unique_key'] = $config['channel_unique_key'];

    Channel\Client::publish('cs_register', $register_data);
  });

  Channel\Client::on('sc_register_' . $config['channel_unique_key'], function ($event_data) use ($inside_worker) {
    if ($event_data['status'] == 1) {
      $inside_worker->inChannel = TRUE;
    }
  });

  Channel\Client::on('cs_connect_' . $config['channel_unique_key'], function ($event_data) use ($inside_worker, $config) {
    $local_host_name = "tcp://" . $config['local_ip'] . ":" . $config['local_port'];

    $connection_to_local = new AsyncTcpConnection($local_host_name);

    $connection_to_local->onConnect = function ($connection) use ($event_data, $config) {
      $connect_data['connection'] = [
        'ip' => $connection->getRemoteIp(),
        'port' => $connection->getRemotePort(),
        'c_connection_id' => $event_data['connection']['c_connection_id'],
      ];
      Channel\Client::publish('sc_connect_' . $config['channel_unique_key'], $connect_data);
    };

    $connection_to_local->onMessage = function ($connection, $data) use ($config, $event_data) {
      // $message_data['session'] = $_SESSION;
      $message_data['data'] = $data;
      $message_data['connection'] = [
        'ip' => $connection->getRemoteIp(),
        'port' => $connection->getRemotePort(),
        'c_connection_id' => $event_data['connection']['c_connection_id'],
      ];

      Channel\Client::publish('sc_message_' . $config['channel_unique_key'], $message_data);
    };

    $connection_to_local->onClose = function ($connection) use ($event_data, $config) {
      // $close_data['session'] = $_SESSION;
      $close_data['connection'] = [
        'ip' => $connection->getRemoteIp(),
        'port' => $connection->getRemotePort(),
        'c_connection_id' => $event_data['connection']['c_connection_id'],
      ];

      Channel\Client::publish('sc_close_' . $config['channel_unique_key'], $close_data);
    };

    $connection_to_local->connect();

    $inside_worker->connections[$event_data['connection']['c_connection_id']] = $connection_to_local;

  });

  Channel\Client::on('cs_message_' . $config['channel_unique_key'], function ($event_data) use ($inside_worker) {
    $inside_worker->connections[$event_data['connection']['c_connection_id']]->send($event_data['data']);
  });
  Channel\Client::on('cs_close' . $config['channel_unique_key'], function ($event_data) use ($inside_worker) {
    if (
      isset($inside_worker->connections[$event_data['connection']['c_connection_id']])
      &&
      $inside_worker->connections[$event_data['connection']['c_connection_id']] != NULL
    ) {
      $inside_worker->connections[$event_data['connection']['c_connection_id']]->close();
    }
  });
};

if (!defined('GLOBAL_START')) {
  Worker::runAll();
}

按照以上代码部署后,在服务端和中转机启动脚本后,即可在服务器直接访问 54321 端口。相当于访问 192.168.1.2 3389 端口。

参考资料:

https://gitee.com/augushong/workerman-port-mapping

https://www.workerman.net/