python aks_使用环回aks和terraform构建基于打字稿的游戏后端
python aks
介紹 (Introduction)
This started as a summer side-project — build an online version of the popular Italian card game Scopa which I called the Scoparella project. Most importantly it was a chance to get hands-on with some technologies I’d been eager to explore in more depth, in particular Azure Kubernetes Service (AKS) and Loopback, a framework for building microservices with Typescript.
這項(xiàng)計(jì)劃最初是從一個(gè)夏季的附帶項(xiàng)目開(kāi)始的-建立一個(gè)流行的意大利紙牌游戲Scopa的在線版本,我稱(chēng)之為Scoparella項(xiàng)目 。 最重要的是,這是一個(gè)機(jī)會(huì),可以親身體驗(yàn)一些我渴望更深入探索的技術(shù),尤其是Azure Kubernetes Service(AKS)和Loopback(使用Typescript構(gòu)建微服務(wù)的框架)。
Scopa is a traditional Italian card game played with a 40-card deck of Italian cards (also playable with the more familiar 52-deck cards by removing the 8, 9 and 10s). The rules are fairly simple, and can be easily found on Google so I won’t digress here. To facilitate this I wrote an engine for the game in Typescript. The API for the engine library is fairly rudimentary:
Scopa是一種傳統(tǒng)的意大利紙牌游戲,使用40張紙牌的意大利紙牌(也可以通過(guò)移除8、9和10來(lái)與更熟悉的52層紙牌一起玩)。 這些規(guī)則非常簡(jiǎn)單,可以在Google上輕松找到,因此我不會(huì)在這里討論。 為了方便起見(jiàn),我用Typescript編寫(xiě)了游戲的引擎 。 引擎庫(kù)的API相當(dāng)初級(jí):
const game = new ScoparellaGame({numberOfPlayers: 2});game.addPlayer(new ScoparellaPlayer("player1"));
game.addPlayer(new ScoparellaPlayer("player2"));
This sets up a game with 2 players…
設(shè)置了一個(gè)有2個(gè)玩家的游戲…
game.tryPlayCards(cardToPlay, cardsToTake, playersHand);…attempts to play a card and take 0…many cards, while playersHand indicates the player playing the hand. Validation is done under the hood to ensure any move is legal before committing it.
…嘗試玩一張紙牌,拿0…許多張紙牌,而playersHand表示玩家在玩手。 驗(yàn)證是在幕后進(jìn)行的,以確保任何舉動(dòng)在提交之前都是合法的。
回送 (Loopback)
Loopback is a rich API framework created by StrongLoop and has no less an entity than IBM backing it. With lots of features you’d expect from any enterprise-grade framework such as the ability to easily protect endpoints that require authentication, all the tools needed to build custom authorisation decorators easily, a CLI tool for scaffolding controllers, services and repositories etc. Loopback allowed me to quickly get productive and setup an API that would allow players to play online!
回送是由StrongLoop創(chuàng)建的豐富API框架,其實(shí)體不亞于IBM支持它的實(shí)體。 您可以從任何企業(yè)級(jí)框架中獲得許多功能,例如能夠輕松保護(hù)需要身份驗(yàn)證的端點(diǎn)的功能,輕松構(gòu)建自定義授權(quán)裝飾器所需的所有工具,用于搭建控制器,服務(wù)和存儲(chǔ)庫(kù)的CLI工具等。讓我快速提高工作效率,并設(shè)置一個(gè)允許玩家在線玩的API!
The first step is to install the Loopback CLI tool:
第一步是安裝Loopback CLI工具:
npm install -g @loopback/cli
npm install -g @loopback/cli
Then simply run lb4, follow the instructions, and Loopback will scaffold all of the bits needed for a microservice. Once this was done I added some services, which was achieved using lb4 service from the root of the application folder. The structure of the src folder where the code lives is given in Figure 2.
然后只需運(yùn)行l(wèi)b4 ,按照說(shuō)明進(jìn)行操作,然后環(huán)回將支持微服務(wù)所需的所有位。 完成此操作后,我添加了一些服務(wù),這些服務(wù)是使用應(yīng)用程序文件夾根目錄中的lb4 service實(shí)現(xiàn)的。 代碼所在的src文件夾的結(jié)構(gòu)如圖2所示。
Figure 2: The src folder of a Loopback application with services added圖2:添加了服務(wù)的Loopback應(yīng)用程序的src文件夾The patterns used here are fairly standard for a CRUD API — controllers, talking to services, which talk to repositories which talk to data stores… the first question one might ask is why can’t I just create this by hand and avoid the need to learn the Loopback framework? As mentioned before Loopback gives you a lot out of the box — it has an ORM framework with wrappers around common data commands and queries so you can interact with a database without having to write a line of SQL; so to update the player in the game with the id of id with the player indicated by player we could do:
此處使用的模式對(duì)于CRUD API來(lái)說(shuō)是相當(dāng)標(biāo)準(zhǔn)的-控制器,與服務(wù)對(duì)話,與存儲(chǔ)庫(kù)對(duì)話,與數(shù)據(jù)存儲(chǔ)對(duì)話……一個(gè)人可能會(huì)問(wèn)的第一個(gè)問(wèn)題是,為什么我不能只手工創(chuàng)建它,而不必學(xué)習(xí)環(huán)回框架? 如前所述,Loopback為您提供了很多開(kāi)箱即用的功能–它具有一個(gè)ORM框架,該框架帶有用于常見(jiàn)數(shù)據(jù)命令和查詢(xún)的包裝器,因此您可以與數(shù)據(jù)庫(kù)進(jìn)行交互而無(wú)需編寫(xiě)SQL。 因此要使用ID為id的游戲者更新游戲中的游戲player我們可以執(zhí)行以下操作:
await super.updateAll({player1:player}, {and: [{id}, {playerId: null}],
});
It can also create entities POJOs (or should than be POTSOs?) using the CLI tool… it adds ES-linting, scaffolds tests, sets up NPM scripts and wires up Swagger, has it’s own IoC framework… and a lot more.
它還可以使用CLI工具創(chuàng)建實(shí)體POJO(或者應(yīng)該是POTSO?)……它添加了ES-lint,腳手架測(cè)試,設(shè)置了NPM腳本并建立了Swagger,具有自己的IoC框架……等等。
OAuth2 (OAuth2)
In order to play an online game you first need to establish some sort of identity. Traditionally this might be achieved by having a database of users that new users can register with, setting a password and verifying their email address using a one-time password (OTP). This is arguably the simplest to develop and test, however more and more service providers from all walks of life are delegating responsibility to well-establish third parties such as Google and Facebook. You’ve probably seen this at some point — you join a site whose services you want to access and they allow you to sign up with one of a select list of providers. There are a number of benefits to this approach — the user journey is simplified, you no longer need to concern yourself with storing potentially sensitive data in a database, users don’t have to manage yet-another-set-of-credentials, and so on.
為了玩在線游戲,您首先需要建立某種身份。 傳統(tǒng)上,這可以通過(guò)擁有新用戶(hù)可以注冊(cè)的用戶(hù)數(shù)據(jù)庫(kù),設(shè)置密碼并使用一次性密碼(OTP)驗(yàn)證其電子郵件地址來(lái)實(shí)現(xiàn)。 可以說(shuō),這是最簡(jiǎn)單的開(kāi)發(fā)和測(cè)試,但是越來(lái)越多的各行各業(yè)的服務(wù)提供商將責(zé)任委托給了建立良好的第三方,例如Google和Facebook。 您可能已經(jīng)看到了這一點(diǎn)-您加入了一個(gè)您想訪問(wèn)其服務(wù)的站點(diǎn),這些站點(diǎn)使您可以通過(guò)選擇的提供者列表之一進(jìn)行注冊(cè)。 這種方法有很多好處-簡(jiǎn)化了用戶(hù)流程,您不再需要擔(dān)心將潛在的敏感數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫(kù)中,用戶(hù)不必管理另一組憑據(jù),并且以此類(lèi)推。
Figure 3: An example of the auth prompt shown to a user attempting to authenticate with OAuth2圖3:向用戶(hù)顯示嘗試通過(guò)OAuth2進(jìn)行身份驗(yàn)證的身份驗(yàn)證提示示例For this project I set up OAuth2 with both Google and Facebook, which involves a bit of work with those providers such as explicitly defining redirect URIs.
對(duì)于這個(gè)項(xiàng)目,我同時(shí)與Google和Facebook一起設(shè)置了OAuth2,這涉及到與這些提供程序的一些工作,例如顯式定義重定向URI。
OAuth2 providers require a callback URL once the client has given their consent, and for a developer this would then entail writing an webhook to handle the callback, authenticating with the provider using a token that was issued to access protected resources (such as the user’s name and email address)... which doesn’t sound like a lot of fun. Luckily For NodeJS developers a library for handling a vast range of common authentication scenarios already exists — passportjs.
一旦客戶(hù)同意,OAuth2提供程序就需要一個(gè)回調(diào)URL,對(duì)于開(kāi)發(fā)人員而言,這將需要編寫(xiě)一個(gè)Webhook來(lái)處理該回調(diào),并使用發(fā)出用于訪問(wèn)受保護(hù)資源(例如用戶(hù)名)的令牌向提供程序進(jìn)行身份驗(yàn)證。和電子郵件地址)...聽(tīng)起來(lái)并不有趣。 幸運(yùn)的是,對(duì)于NodeJS開(kāi)發(fā)人員來(lái)說(shuō),已經(jīng)存在一個(gè)用于處理各種常見(jiàn)身份驗(yàn)證方案的庫(kù)– passwordjs 。
Passport strategies (as they are know) do not work out of the box with Loopback (it targets Express), but can be made to work using an adapter pattern that involves wrapping native passport strategies in a Loopback middleware that calls down to native passport authentication; I took inspiration from this example. This middleware is then invoked on callback and the correct strategy used to authenticate. A signed JWT is issued which can be used to take part in games by providing it in the Authentication header as a bearer token. Because it is a JWT no further interaction with the provider is necessary while the token is valid, and it is signed so the claims cannot be tampered with.
護(hù)照策略(眾所周知)不能與Loopback配合使用(針對(duì)Express),但可以使用適配器模式來(lái)工作,該適配器模式包括將本機(jī)護(hù)照策略包裝在Loopback中間件中,從而調(diào)用本機(jī)護(hù)照身份驗(yàn)證; 我從這個(gè)例子中得到了啟發(fā)。 然后在回調(diào)上調(diào)用此中間件,并使用正確的策略進(jìn)行身份驗(yàn)證。 發(fā)出簽名的JWT,可以通過(guò)將其作為Authentication令牌提供在Authentication標(biāo)頭中來(lái)參與游戲。 由于它是JWT,因此令牌有效時(shí)就不需要與提供者進(jìn)行進(jìn)一步的交互,并且令牌已簽名,因此不能篡改聲明。
Figure 4: Loopback/Passport adapter圖4:環(huán)回/護(hù)照適配器采取行動(dòng) (Making a move)
The data store is fairly basic — a SQL Server table stores all games in progress and a serialised copy of the state of a game. When a player makes a move, the game is pulled out of the database, deserialised (using the Scopa engine I’d built), the move is played, the game is serialised and saved back into the database. The Engine acts as a state machine and will only allow valid moves (for example a player can only move when it is their turn).
數(shù)據(jù)存儲(chǔ)是相當(dāng)基本的-SQL Server表存儲(chǔ)所有進(jìn)行中的游戲和游戲狀態(tài)的序列化副本。 當(dāng)玩家進(jìn)行移動(dòng)時(shí),將游戲從數(shù)據(jù)庫(kù)中拉出,進(jìn)行反序列化(使用我構(gòu)建的Scopa引擎),進(jìn)行移動(dòng),將游戲序列化并保存回?cái)?shù)據(jù)庫(kù)中。 引擎充當(dāng)狀態(tài)機(jī),并且僅允許有效移動(dòng)(例如,玩家只能在回合時(shí)移動(dòng))。
測(cè)試中 (Testing)
The application can be spun up locally and a game can be played given valid JWT tokens. For automated testing I stubbed out the auth providers and used docker compose to create a) the application and b) the database, within a local Docker network where acceptance tests can be run.
可以在本地啟動(dòng)該應(yīng)用程序,并且可以使用有效的JWT令牌來(lái)玩游戲。 對(duì)于自動(dòng)化測(cè)試,我在Docker本地網(wǎng)絡(luò)中運(yùn)行身份驗(yàn)證提供程序并使用docker compose創(chuàng)建a)應(yīng)用程序和b)數(shù)據(jù)庫(kù)。
在Azure中托管 (Hosting in Azure)
As a side-project one of my goals is to get hands-on with technologies that are interesting or likely to be valuable in my day-to-day work. As such, I decided to host the application in Azure on AKS (other options are available in Azure, notably Web App for Containers). I would need to setup a Kubernetes cluster in Azure, which costs a few pounds a week to keep running. Luckily my current employer gives me some free Azure credits every month as part of my MSDN subscription, so I was able to use these.
作為附帶項(xiàng)目,我的目標(biāo)之一是動(dòng)手使用有趣或可能對(duì)我的日常工作有價(jià)值的技術(shù)。 因此,我決定將應(yīng)用程序托管在AKS上的Azure中(其他選項(xiàng)在Azure中可用,尤其是容器Web應(yīng)用程序 )。 我需要在Azure中設(shè)置一個(gè)Kubernetes集群,每周花費(fèi)幾英鎊來(lái)保持運(yùn)行。 幸運(yùn)的是,作為MSDN訂閱的一部分,我現(xiàn)在的老板每月都會(huì)給我一些免費(fèi)的Azure信用,因此我可以使用這些信用。
I decided to use Terraform, a tool for infrastructure-as-code that is gaining popularity in the world of devops. These days creating infrastructure using point-and-click user interfaces is frowned upon in most scenarios for good reasons that are beyond the scope of this article. Scripts can also accomplish these tasks, but lack the state-tracking of Terraform, which is a powerful tool for ensuring changes to code can easily be reflected by the provisioned resources.
我決定使用Terraform,這是一種用于將基礎(chǔ)結(jié)構(gòu)作為代碼的工具,在devop的世界中越來(lái)越受歡迎。 如今,在大多數(shù)情況下,使用點(diǎn)擊用戶(hù)界面創(chuàng)建基礎(chǔ)結(jié)構(gòu)的想法已經(jīng)超出了本文的討論范圍。 腳本也可以完成這些任務(wù),但是缺少Terraform的狀態(tài)跟蹤,Terraform是一種功能強(qiáng)大的工具,可確保已調(diào)配的資源可以輕松反映代碼的更改。
To give you an example of it’s succinctness, this is all it takes to spin up a Kubernetes cluster comprising 2 nodes:
舉一個(gè)簡(jiǎn)潔的例子,這就是啟動(dòng)一個(gè)包含2個(gè)節(jié)點(diǎn)的Kubernetes集群的全部工作:
resource "azurerm_kubernetes_cluster" "scoparella-kube" {name = "${var.environment}-scoparella-aks1"
location = var.location
resource_group_name = var.resource_group_name
dns_prefix = "${var.environment}scoparellaaks1"
node_resource_group = "${var.environment}-scoparella-aks-rg"
default_node_pool {
name = "agentpool"
node_count = 2
vm_size = "Standard_B2s"
} identity {
type = "SystemAssigned"
} lifecycle {
prevent_destroy = false
} tags = {
environment = var.environment
}
}
Terraform resources can be organised into modules, and I also had modules for resources such as KeyVault which was used to store the database password among other things.
Terraform資源可以組織為模塊,我還具有諸如KeyVault之類(lèi)的資源模塊,這些模塊曾用于存儲(chǔ)數(shù)據(jù)庫(kù)密碼。
In addition to terraform I also defined the application deployment using the standard Kubernetes YAML format; I did experiment with defining these natively in HashiCorp Configuration Language (HCL — the configuration language of Terraform), but opted to use YAML instead as it was more familiar to me, and better supported and documented. Database objects (DBOs) were applied using sqlcmd .
除了terraform之外,我還使用標(biāo)準(zhǔn)的Kubernetes YAML格式定義了應(yīng)用程序部署。 我確實(shí)嘗試過(guò)使用HashiCorp配置語(yǔ)言(HCL-Terraform的配置語(yǔ)言)本地定義它們,但是選擇使用YAML代替,因?yàn)樗鼘?duì)我來(lái)說(shuō)比較熟悉,并且得到了更好的支持和記錄。 使用sqlcmd應(yīng)用數(shù)據(jù)庫(kù)對(duì)象(DBO)。
I faced several challenges during the process of scripting up the infrastructure. The biggest obstacle was finding a way to access secrets held in KeyVault. Kubernetes has its own approaches to secrets management, however I chose to use a project called AAD Pod Identity which allows Kubernetes applications to access resources in Azure (such as KeyVault) by authenticating with Azure Active Directory and assuming a role with the privileges required. I created roles and identities using terraform, but applied the deployment of the identity pods using Kubernetes YAML and some bash to extract the relevant UUIDs dynamically. On application startup the Scopa API application it will pull the secrets it needs from KeyVault and cache them locally.
在編寫(xiě)基礎(chǔ)結(jié)構(gòu)腳本的過(guò)程中,我遇到了一些挑戰(zhàn)。 最大的障礙是找到一種方法來(lái)訪問(wèn)KeyVault中保存的機(jī)密。 Kubernetes具有自己的秘密管理方法,但是我選擇使用一個(gè)名為AAD Pod Identity的項(xiàng)目,該項(xiàng)目允許Kubernetes應(yīng)用程序通過(guò)向Azure Active Directory進(jìn)行身份驗(yàn)證并承擔(dān)具有所需特權(quán)的角色來(lái)訪問(wèn)Azure中的資源(例如KeyVault)。 我使用terraform創(chuàng)建了角色和身份,但是使用Kubernetes YAML和一些bash應(yīng)用了身份吊艙的部署來(lái)動(dòng)態(tài)提取相關(guān)的UUID。 在應(yīng)用程序啟動(dòng)時(shí),Scopa API應(yīng)用程序?qū)腒eyVault中提取所需的機(jī)密并將其本地緩存。
Finally I applied a cloud-network load balancer, binding a public IP address (on port 80) to the internal IP address of the service (on port 3000). With this in place I could verify liveness over the public internet using curl to a /ping endpoint, confirming the cluster was setup correctly.
最后,我使用了一個(gè)云網(wǎng)絡(luò)負(fù)載平衡器,將一個(gè)公共IP地址(在端口80上)綁定到該服務(wù)的內(nèi)部IP地址(在端口3000上)。 有了這個(gè)適當(dāng)?shù)奈恢?#xff0c;我可以使用curl到/ping端點(diǎn)來(lái)驗(yàn)證公共互聯(lián)網(wǎng)上的活動(dòng),確認(rèn)集群已正確設(shè)置。
apiVersion: v1kind: Service
metadata:
name: "scoparella-api-service"
spec:
type: LoadBalancer
selector:
app: scoparella-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
API管理服務(wù) (API Management Services)
Clearly accessing a backend via it’s public IP address over an insecure connection wasn’t a desirable end-state. I used API Management Services to act as a reverse proxy to the load balancer service (over HTTPS). To do this I needed to add the API Management service to the AKS VNet, allocate an address space for it, and select the subnet from the VNet configuration of the API Management Service (Figure 5). There are challenges doing this at present with Terraform so I was forced to use the Azure UI. Scripting this up is on my todo list, even if just in a bash script.
顯然,通過(guò)不安全的連接通過(guò)后端的公共IP地址訪問(wèn)后端不是理想的最終狀態(tài)。 我使用API?? Management Services充當(dāng)負(fù)載平衡器服務(wù)(通過(guò)HTTPS)的反向代理。 為此,我需要將API Management服務(wù)添加到AKS VNet,為其分配地址空間,然后從API Management Service的VNet配置中選擇子網(wǎng)(圖5)。 目前,使用Terraform會(huì)遇到很多挑戰(zhàn),因此我被迫使用Azure UI。 即使在bash腳本中,也可以在此腳本上編寫(xiě)腳本。
Figure 5: Pointy-clicky for setting API Management VNet Subnet圖5:設(shè)置API管理VNet子網(wǎng)的鼠標(biāo)單擊Azure開(kāi)發(fā)人員 (Azure Devops)
Finally for CI/CD I chose to use Azure Devops. This is a tool we are using extensively in my current role, and it has the advantage of being fully managed and it integrates well with the rest of the Azure ecosystem. Pipelines are defined using YAML, and a base image can be selected to fit particular requirements. I chose Ubuntu, and setup an Azure pipeline to checkout the code, run tests inside a container, terraform, and apply deployments using kubectl.
最后,對(duì)于CI / CD,我選擇使用Azure Devops。 這是我目前擔(dān)任職務(wù)時(shí)廣泛使用的工具,它具有受到完全管理的優(yōu)勢(shì),并且與Azure生態(tài)系統(tǒng)的其余部分很好地集成在一起。 管道是使用YAML定義的,并且可以選擇基本圖像以滿足特定要求。 我選擇了Ubuntu,并設(shè)置了Azure管道來(lái)簽出代碼,在容器中運(yùn)行測(cè)試,terraform并使用kubectl應(yīng)用部署。
These are defined as “tasks” in an Azure Devops pipeline. An example of one is given below:
這些在Azure Devops管道中被定義為“任務(wù)”。 下面是一個(gè)示例:
- task: Bash@3condition: or( eq('${{ parameters.task }}', '*'), eq('${{ parameters.task }}', 'terraform') )
inputs:
targetType: "inline"
script: |
wget https://releases.hashicorp.com/terraform/0.12.29/terraform_0.12.29_linux_amd64.zip
unzip terraform_0.12.29_linux_amd64.zip
./terraform init
./terraform plan
./terraform apply -auto-approve
workingDirectory: "./resources/${{ parameters.stage }}"
displayName: "Terraform data/secrets"
While this is just some bash, Devops also has templates for common task, for example building and pushing a Docker image:
盡管這只是一些麻煩,但Devops還具有用于常見(jiàn)任務(wù)的模板,例如,構(gòu)建和推送Docker映像:
- task: Docker@2displayName: Build and Push
inputs:
containerRegistry: "dockerscoparella"
repository: "garrypassarella/scoparella"
command: "buildAndPush"
Dockerfile: "Dockerfile"
tags: |
scoparella_api_${{ variables.env }}
condition: or( eq('${{ parameters.task }}', '*'), eq('${{ parameters.task }}', 'docker') )
One caveat is that Azure Devops requires an identity in Azure Active Directory with the privileges necessary to provision resources in Azure; my organisation do not grant me the ability to create these, so I was forced to do an Azure login whenever I wanted to run a build — a bit clunky, but something I wouldn’t face if I owned the subscription. For similar reasons I was unable to use Azure Container Registry (ACR) but I got around this by using Docker Hub and setting up a service connection.
一個(gè)警告是,Azure Devops要求在Azure Active Directory中具有一個(gè)身份,該身份具有在Azure中置備資源所必需的特權(quán); 我的組織沒(méi)有授予我創(chuàng)建這些功能的能力,因此,每當(dāng)我要運(yùn)行構(gòu)建時(shí),我都被迫進(jìn)行Azure登錄–有點(diǎn)笨拙,但是如果擁有訂閱,我將不會(huì)面對(duì)。 出于類(lèi)似的原因,我無(wú)法使用Azure容器注冊(cè)表(ACR),但是通過(guò)使用Docker Hub并設(shè)置服務(wù)連接來(lái)解決此問(wèn)題。
下一步? (Next steps?)
Unfortunately my Azure credits ran out after 2 weeks (implying I might need to swat up on cost optimisation next, or that Azure is just very expensive). I still have some things I’d like to do to this project, in particular script out the API Management parts. In the meantime I am looking at building a web-based front-end for the game using React. That might be the subject of another article in a few months…
不幸的是,我的Azure信用額度在2周后就用光了(這意味著我接下來(lái)可能需要進(jìn)行成本優(yōu)化,否則Azure會(huì)非常昂貴)。 我仍然想對(duì)此項(xiàng)目做一些事情,特別是將API管理部分腳本化。 同時(shí),我正在考慮使用React構(gòu)建游戲的基于Web的前端。 這可能是幾個(gè)月后另一篇文章的主題。
The full code for the API is here: https://github.com/garrypas/scoparella.api
API的完整代碼在這里: https : //github.com/garrypas/scoparella.api
For the Scoparella Engine here: https://github.com/garrypas/scoparella.engine
對(duì)于此處的Scoparella引擎: https : //github.com/garrypas/scoparella.engine
翻譯自: https://medium.com/@garry.passarella/building-a-typescript-based-gaming-back-end-with-loopback-aks-and-terraform-b533c9485e80
python aks
總結(jié)
以上是生活随笔為你收集整理的python aks_使用环回aks和terraform构建基于打字稿的游戏后端的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 手机gnss定位相关知识
- 下一篇: Python实现数列求和