# Anser-Gateway開發守則
###### tags: `Anser`
[TOC]
## Code Style
* 命名原則以 **`駝峰`** 命名為主
* 變數命名方式以描述清楚為目標,例如:
```php=
class ServiceDiscovery
{
/**
* ServiceDiscovery 設定檔實體
*
* @var \Config\ServiceDiscovery
*/
protected $serviceDiscoveryConfig;
/**
* GatewayRegister 設定檔實體
*
* @var GatewayRegister
*/
protected $gatewayRegister;
/**
* 須被訪問的服務
*
* @var array
*/
protected array $defaultServiceGroup = [];
/**
* 重新搜尋的等待Timer
*
* @var integer
*/
public int $reloadTime;
```
每一變數命名方式皆為變數的職責為何。
* 變數命名後,**`註解須清楚標記該變數型別為何`**,如int則為integer(Line 24~29),若為類別(Line 3~8),則標記類別名稱。
* 方法撰寫原則與上述相似,方法名稱以描述行為為主;若方法具備傳入子,則須明確標記傳入子型別(Line 7),註解部分盡可能撰寫清楚該方法職責為何,並標記`param`和`return` 型別,若無回傳值`return`則回傳void。可特別注意Line 7 結尾的`:void`,此處為標記回傳型別,記得加上,讓PHP編譯檢查。
```php=
/**
* 如服務回傳多組service的話則使用該方法
*
* @param array $services
* @return void
*/
public function setServices(array $services): void
{
foreach ($services as $service) {
$serviceEntity = $service["Service"];
$serviceTags = $serviceEntity["Tags"];
$serviceName = $serviceEntity["Service"];
$serviceAddress = $serviceEntity["Address"];
$servicePort = $serviceEntity["Port"];
$serviceSchemeIsHttp = false;
foreach ($serviceTags as $tag) {
if (strpos($tag, 'http_scheme=') === 0) {
$serviceScheme = substr($tag, strlen('http_scheme='));
$serviceSchemeIsHttp = strtolower($serviceScheme) == 'http' ? false : true;
break;
}
}
$this->localServices[$serviceName][] = [
"name" => $serviceName,
"address" => $serviceAddress,
"port" => $servicePort,
"scheme" => $serviceSchemeIsHttp
];
}
}
```
## AnserGateway 生命週期
### 角色描述
:::success
* Client : 通常指外部使用者或對Anser-Gateway發起請求的人
* GatewayWorker.php : Anser-Gateway運行關鍵,初始化Workerman的Worker,運行Anser-Gateway邏輯
* Anser-Gateway Class : Anser-Gateway處理HTTP Request/Response 的類別,內部將Request進行拆解後導向Routes、Filters、Controllers等元件解析請求。
* Router Class : 路由器,根據傳入的Request URI找尋User從config/Routes.php定義的路由,且呼叫所定義的Controller與Filter
* Filter Class : 過濾器,分為before、After Filter,Before作用於Router Class後,Controller Class前,主要作某些不宜實作於Controller層的邏輯,例如登入驗證等;After作用於Controller Class後,適合做一些回傳Response前的驗證。
* Controller Class : 完整闡述業務邏輯之處,如與微服務溝通、呼叫Orchestration等行為。
* Response : 由Controller Class建構時產生(此時為空),Controller Class作用結束後會產生第一階段Response,若Controller Class完成後有綁定After Filter ,則會將Response帶入Filter中進行After Filter實作,最終回傳Response;若未綁定After Filter,則將直接回傳Response。
:::

圖1. Anser-Gateway的Request/Response處理邏輯

圖2. Anser-Gateway元件圖
## 關於新類別的開發
### 設定檔
:::danger
若實作類別不存在開發人員自訂義類別參數,則可略過此段。
:::
在軟體開發中,某些系統參數是私密或個人化的,故不宜直接被Hard Code於類別或功能模組中,故採環境變數(Environment-Variables, ENV)進行帶入。[關於ENV](https://www.w3schools.in/php/environment-variables)
#### Env在Anser-Gateway中的實踐
:::success
主要檔案位置
system/Config/BaseConfig.php
system/Config/DotEnv.php
:::
##### DotEnv.php
該檔案主要職責為將我們在.env檔案中定義的環境設定變數存入到memory中,讓PHP呼叫;會在anser啟動時被初始化。
##### BaseConfig.php
所有涉及環境變數的類別設定皆須繼承該類別,該類別主要職責為讀取類別變數,並將.env檔案中的參數賦值至類別中。
以服務探索為例 `config/ServiceDiscovery.php`,類別於Line 7繼承了BaseConfig,並存在一些變數未被賦值,此時可觀察下面的.env檔案,內部定義皆與`ServiceDiscovery.php`相同,該處須注意的為,在.env中註冊變數時記得以`類別名稱.變數名稱`的方式進行定義,如: `servicediscovery.defaultServiceGroup`
```php=
<?php
// config/ServiceDiscovery.php
namespace Config;
use AnserGateway\Config\BaseConfig;
class ServiceDiscovery extends BaseConfig
{
/**
* 需要被探索的服務名稱
*
* @var array<string>
*/
public array $defaultServiceGroup = [];
/**
* Consul Server IP Address and port
*
* @var string
*/
public string $address = 'http://localhost:8500';
/**
* HTTP Scheme [http or https]
*
* @var string
*/
public string $scheme = 'http';
/**
* Consul Server DataCenter
*
* @var string
*/
public string $dataCenter = '';
/**
* 請求間隔
*
* @var integer
*/
public int $reloadTime = 10;
/**
* 服務負載均衡演算法
*
* @var string
*/
public string $LBStrategy = 'random';
public function __construct()
{
parent::__construct();
// 因.env傳入為字串,故使用explode作切割
if(getenv('servicediscovery.defaultServiceGroup') !== ''){
$this->defaultServiceGroup = explode(',', getenv('servicediscovery.defaultServiceGroup'));
}
}
}
```
```env=
#.env
#--------------------------------------------------------------------
# Service Discovery
#--------------------------------------------------------------------
# servicediscovery.defaultServiceGroup = ServiceName1,ServiceName2 # [required] Same name as the service registered on Consul, If you have more than one service, please separate them with a comma(,).
# servicediscovery.address = 'localhost:8600' # [required]
# servicediscovery.scheme = 'http' # [optional] defaults to "http"
# servicediscovery.dataCenter = '' # [optional]
# servicediscovery.reloadTime = 300 # [optional] php json encode opt value to use when serializing requests
# servicediscovery.LBStrategy = random
```
### 類別中呼叫外部API
此時建議使用Anser-Action進行操作,若直接使用GuzzleHTTP的話會出現小複雜的效能負擔(這部份往Coroutine和HTTP連線管理去了)。
#### 實際撰寫方式
以`system/ServiceDiscovery/ServiceDiscovery.php`為例,下述程式碼為Anser-Gateway啟動時將自己註冊至Consul中的步驟,此處直接使用Anser-Action進行實現,可有效使用HTTP連線管理。
```php=
/**
* 註冊AnserGateway 至 Consul Server
*
* @param string $httpScheme
* @param integer $port
* @return bool
*/
public function registerSelf(string $httpScheme, int $port): bool
{
$gatewayAddress = sprintf(
'%s://%s:%s',
$httpScheme,
$this->gatewayRegister->address,
$port
);
$checkRoute = sprintf(
'%s/%s',
$gatewayAddress,
$this->gatewayRegister->healthRoute
);
array_push($this->gatewayRegister->tags, "http_scheme={$httpScheme}");
$action = (new Action(
$this->consulAddress,
"PUT",
"v1/agent/service/register"
))->addOption("json", [
"id" => $this->gatewayRegister->id,
"name" => $this->gatewayRegister->name,
"tags" => $this->gatewayRegister->tags,
"address" => $this->gatewayRegister->address,
"port" => (int)$port,
"check" => [
"name" => $this->gatewayRegister->name,
"service_id" => $this->gatewayRegister->id,
"http" => $checkRoute,
"interval" => $this->gatewayRegister->interval,
"timeout" => $this->gatewayRegister->timeout
]
])->doneHandler(function (
ResponseInterface $response,
Action $runtimeAction
) {
$body = $response->getBody()->getContents();
$data = json_decode($body, true);
$runtimeAction->setMeaningData($data);
})->failHandler(function (
ActionException $e
) {
if($e->isClientError()) {
$e->getAction()->setMeaningData([
"code" => $e->getStatusCode(),
"msg" => "client error"
]);
} elseif ($e->isServerError()) {
$e->getAction()->setMeaningData([
"code" => $e->getStatusCode(),
"msg" => "server error"
]);
} elseif($e->isConnectError()) {
$e->getAction()->setMeaningData([
"msg" => $e->getMessage()
]);
}
});
$data = $action->do()->getMeaningData();
if (isset($data['msg'])) {
throw \AnserGateway\ServiceDiscovery\Exception\ServiceDiscoveryException::forAnserGatewayRegisterError($data);
}
/**
* Consul 註冊成功回傳為null
*/
if(is_null($data)) {
return true;
}
return false;
}
```
### 類別初始化位置
若是元件規模較大或為單例,建議於`system/Worker/GatewayWorker.php`進行初始化。
簡單介紹GatewayWorker的週期,主要有兩個時期
:::success
一、$webWorker->onWorkerStart
這邊簡單來說就是當Worker被建立時,需要做些甚麼,很像JS的callable函式實作方式。
二、$webWorker->onMessage
當Worker接收到外部Request時會動作的部分,此處也可以用JS的callable去理解,當某個按鈕被按到時就觸發。
此處放置較重要的功能模組,如AnserGateway Class
:::
* 以Line 30的`$serviceDiscovery`為例,變數屬性為static(靜態)並賦予初始值null(盡量給初始值)。
* 變數初始化周期在於`onWorkerStart`時,參考Line 72~75。
* 若功能需對每一請求進行處理則可放在`onMessage`執行,視功能規模必要性實作於此,若可實作於`runtimeTcpConnection`或`Filter`的話盡量實作在那邊。
```php=
<?php
namespace AnserGateway\Worker;
use Config\Gateway;
use Swow\Coroutine;
use Workerman\Timer;
use Workerman\Worker;
use AnserGateway\Autoloader;
use AnserGateway\Worker\Swow;
use AnserGateway\AnserGateway;
use AnserGateway\Router\Router;
use Workerman\Protocols\Http\Request;
use Workerman\Protocols\Http\Response;
use SDPMlab\Anser\Service\ServiceList;
use AnserGateway\Router\RouteCollector;
use Workerman\Connection\TcpConnection;
use AnserGateway\HTTPConnectionManager;
use AnserGateway\Worker\WorkerRegistrar;
use AnserGateway\ServiceDiscovery\ServiceDiscovery;
class GatewayWorker extends WorkerRegistrar
{
protected Gateway $gatewayConfig;
public static $routeList;
public static $router;
public static $serviceDiscovery = null;
public function __construct()
{
$this->gatewayConfig = new Gateway();
self::staticSetting();
}
public function initWorker(): Worker
{
$config = $this->gatewayConfig;
$webWorker = new Worker(
sprintf(
'%s://%s:%s',
$config->ssl ? 'https' : 'http',
'0.0.0.0',
$config->listeningPort
),
$this->gatewayConfig->ssl ? [
'ssl' => [
'local_cert' => $config->sslCertFilePath,
'local_pk' => $config->sslKeyFilePath,
'verify_peer' => $config->sslVerifyPeer,
'allow_self_signed' => $config->sslAllowSelfSigned,
],
] : []
);
$webWorker->name = 'AnserGateway';
$webWorker->reloadable = true;
$this->instanceSetting($webWorker);
// On start
$webWorker->onWorkerStart = static function (Worker $worker) use ($config) {
Autoloader::$instance->appRegister();
Autoloader::$instance->composerRegister();
require_once PROJECT_CONFIG . 'Service.php';
//此處開始框架其他部件初始化
\AnserGateway\Worker\GatewayWorker::$routeList = RouteCollector::loadRoutes();
\AnserGateway\Worker\GatewayWorker::$router = new Router(\AnserGateway\Worker\GatewayWorker::$routeList);
if ($config->enableServiceDiscovery) {
\AnserGateway\Worker\GatewayWorker::$serviceDiscovery = new ServiceDiscovery();
\AnserGateway\Worker\GatewayWorker::$serviceDiscovery->registerSelf($config->ssl ? 'https' : 'http', $config->listeningPort);
}
ServiceList::setGlobalHandlerStack(HTTPConnectionManager::connectionMiddleware());
HTTPConnectionManager::$hostMaxConnectionNum = 150;
HTTPConnectionManager::$waitConnectionTimeout = 200;
// Timer包co ,實作服務發現邏輯...
if (!is_null(\AnserGateway\Worker\GatewayWorker::$serviceDiscovery)) {
// first discovery
ServiceList::setServiceDataHandler(\AnserGateway\Worker\GatewayWorker::$serviceDiscovery->serviceDataHandler());
// 預先探索服務
\AnserGateway\Worker\GatewayWorker::$serviceDiscovery->doServiceDiscovery();
Timer::add(
\AnserGateway\Worker\GatewayWorker::$serviceDiscovery->reloadTime,
static function () {
Coroutine::run(static function (): void {
\AnserGateway\Worker\GatewayWorker::$serviceDiscovery->doServiceDiscovery();
});
}
);
}
};
// Worker
$webWorker->onMessage = static function (TcpConnection $connection, Request $request) use ($config) {
Coroutine::run(static function () use ($connection, $request, $config): void {
$config->runtimeTcpConnection($connection, $request);
# Injection Router class to AnserGateway
$gateway = new AnserGateway(\AnserGateway\Worker\GatewayWorker::$router);
try {
$workermanResponse = $gateway->handleRequest($request);
} catch (\Exception $e) {
$workermanResponse = new Response(
500,
[
'Content-Type' => 'application/json charset=utf-8',
],
json_encode([
'code' => 500,
'msg' => $e->getMessage(),
'data' => null
])
);
}
$connection->send($workermanResponse);
unset($gateway);
});
};
return $webWorker;
}
protected function instanceSetting(Worker &$worker)
{
$worker->count = $this->gatewayConfig->workerCount;
$worker->user = $this->gatewayConfig->workerUser;
$this->gatewayConfig->initWorker($worker);
}
public function staticSetting()
{
Worker::$eventLoopClass = Swow::class;
Worker::$stdoutFile = $this->gatewayConfig->stdoutFile;
Worker::$logFile = $this->gatewayConfig->logFile;
TcpConnection::$defaultMaxPackageSize = $this->gatewayConfig->defaultMaxPackageSize;
TcpConnection::$defaultMaxSendBufferSize = $this->gatewayConfig->defaultMaxSendBufferSize;
}
}
```