golang使用阿里云OSS对象存储
概念
Bucket
:存储空间Object
:文件ObjectKey
:文件名,文件的唯一标识,与Key
、ObjectName
为同一概念Region
:地域,OSS数据中心的地理位置Endpoint
:域名AccessKey
:密钥,由AccessKey ID
和AccessKey Secret
组成
=
存储费 +
临时存储费 +
数据取回费 +
流量费 +
请求费 +
其他费用一、存储费
存储费 = 文件大小(GB)
x
单价 ÷
30 ÷
24 x
存储时长(hour)存储类型 | 单价 |
---|---|
标准-本地冗余 | 0.12 |
标准-同城冗余 | 0.15 |
低频访问-本地冗余 | 0.08 |
低频访问-同城冗余 | 0.10 |
归档 | 0.033 |
冷归档 | 0.015 |
最贵的标准存储的单价是最便宜的冷归档存储的二、临时存储费8
倍。
存储一个大小为1G
的文件每月需要支付0.12
元的存储费。
临时存储费 = 文件大小(GB)
x
单价 ÷
30 ÷
24 x
解冻时长(hour)存储类型 | 单价 |
---|---|
冷归档 | 0.12 |
冷归档文件在解冻期间,须额外支付一笔按照标准存储单价计算的临时存储费用。三、数据取回费
【golang使用阿里云OSS对象存储】数据取回费 = 文件大小(GB)
x
单价存储类型 | 单价 |
---|---|
标准 | 0 |
低频访问 | 0.0325 |
归档 | 0.06 |
冷归档 | 0.03~0.2 |
流量费 = 流量(GB)
x
单价流量方向 | 单价 |
---|---|
上传 | 0 |
内网下载 | 0 |
外网下载(0-8) | 0.25 |
外网下载(8-24) | 0.50 |
CDN回源下载 | 0.15 |
跨区域复制(中国大陆) | 0.50 |
外网下载(0-8) | 0.25 |
上传免费,下载收费。五、请求费
一个大小为1G
的文件,每下次1
次,需要支付0.25
元的流量费。
请求费 =
math.Ceil(
请求次数/10000)
x
单价请求类型 | 标准 | 低频访问 | 归档 | 冷归档 |
---|---|---|---|---|
PUT | 0.01 | 0.1 | 0.1 | 0.1 |
GET | 0.01 | 0.1 | 0.1 | 0.1 |
取回 | / | / | / | 0.3 ~ 30 |
冷归档文件的取回请求每一万次需支付30
元的请求费。
六、其他费用跨区域文件复制流量费、图片处理费、图片压缩费、视频截帧费、标签费、传输加速费、DDoS防护费、元数据管理费、敏感数据保护费等等。
存储类型 标准 + 低频 + 归档 + 冷归档。
文章图片
对比指标 | 标准 | 低频 | 归档 | 冷归档 |
---|---|---|---|---|
最小计量单位 | 0 | 64KB | 64KB | 64KB |
最小存储时长 | 0 | 30天 | 60天 | 180天 |
访问延迟 | 0 | 0 | 1min | 1-12hour |
数据取回费用 | 0 | 30天 | 60天 | 180天 |
://./
私有:
://./
Bucket CRUD
Bucket
创建后不支持修改,所以只有CRD
,没有U
。package mainimport (
"fmt"
"os"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)func main() {
// 创建OSSClient实例。
// yourEndpoint填写Bucket对应的Endpoint。
// 以华东1(杭州)为例,填写为https://oss-cn-hangzhou.aliyuncs.com。
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 新增
err = client.CreateBucket(
"examplebucket",
oss.StorageClass(oss.StorageIA),
oss.ACL(oss.ACLPublicRead),
oss.RedundancyType(oss.RedundancyZRS)
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
/*
StorageClass:存储类型。
StorageStandard=标准存储
StorageIA=低频访问
StorageArchive=归档存储
StorageColdArchive=冷归档存储
ACL:访问权限。
ACLPrivate=私有
ACLPublicRead=公共读
ACLPublicReadWrite=公共读写
RedundancyType:冗余类型。
RedundancyLRS=本地冗余
RedundancyZRS=同城冗余
*/// 查询
marker := ""
for {
lsRes, err := client.ListBuckets(oss.Marker(marker))
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 默认情况下一次返回100条记录。
for _, bucket := range lsRes.Buckets {
fmt.Println("Bucket: ", bucket.Name)
}if lsRes.IsTruncated {
marker = lsRes.NextMarker
} else {
break
}
}// 查询Region ID
regionID, err := client.GetBucketLocation("examplebucket")
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
fmt.Println("Bucket Location:", regionID) // oss-cn-shanghai
/*
Region ID可以理解为Endpoint的前缀:
外网Endpoint:oss-cn-shanghai.aliyuncs.com
内网Endpoint:oss-cn-shanghai-internal.aliyuncs.com
*/// 删除
err = client.DeleteBucket("examplebucket")
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
}
生命周期
通过设置生命周期,可以实现定期将
Bucket
内的Object
转换存储类型,或者将过期的Object
和碎片文件删除,进而降低存储费用。可以基于最后一次修改时间(Last Modified Time)或最后一次访问时间(Last Access Time)创建生命周期规则。
限制 | 最后一次修改时间 | 最后一次访问时间 |
---|---|---|
适用场景 | 访问模型确定 | 访问模型不确定 |
是否支持删除Object | 支持 | 不支持 |
存储类型的转换是否可逆 | 不可逆 | 可逆 |
这样就可以实现类似「上传N天后删除」或者「上传N天后将存储类型由标准切换为低频访问」的需求。
一些限制
- 仅支持根据前缀和标签进行匹配,不支持通配符匹配、后缀匹配以及正则匹配。
- 可以叠加,但上限为
1000
条。 - 溯及既往
- 生效:规则创建成功后的
24
小时内完成加载,加载成功后将于每天的08:00
执行。 - 最后修改时间与规则执行时间的间隔必须大于
24
小时。
package mainimport (
"fmt"
"os"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)func Main() {
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 定义规则// 前缀为10086的Object将于最后一次修改时间后的第14天删除
ruleA := oss.BuildLifecycleRuleByDays("ruleA", "10086", true, 14)// 前缀为10010的Object将于2025年10月10日的08:00删除
ruleB := oss.BuildLifecycleRuleByDate("ruleB", "10010", true, 2025, 10, 10)// 前缀为10000的Object的存储类型将于最后一次修改时间的30天后变更为低频访问
ruleC := oss.LifecycleRule{
ID:"ruleC",
Prefix: "10000",
Status: "Enabled",
Transitions: []oss.LifecycleTransition{
{
Days:30,
StorageClass: oss.StorageIA,
},
},
}// 设置规则
err = client.SetBucketLifecycle(
"examplebucket", []oss.LifecycleRule{ruleA, ruleB, ruleC},
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
}
基于最后一次访问时间 基于最后一次访问时间的生命周期规则似乎不支持
SDK
。Object 类型
文章图片
Object
根据上传方式可以分为以下三类:上传方式 | 类型 |
---|---|
简单上传 | Normal |
分片上传 | Multipart |
追加上传 | Appendable |
UTF-8
编码1 ~ 1024
个字符- 不能以
/
和\
开头
package mainimport (
"fmt"
"os"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)func Main() {
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 生成Bucket实例
bucket, e := client.Bucket("examplebucket")
if err != nil {
fmt.Println("Error:", e)
os.Exit(-1)
}// 上传
if err = bucket.PutObject("objectKey", file);
err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 删除
if err = bucket.DeleteObject("objectKey");
err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
}
访问控制 阿里云提供四种类型的访问权限控制策略:
类型 | 约束对象 |
---|---|
RAM Policy | OSS服务所属阿里云主账号的RAM |
Bucket Policy | 所有阿里云主账号的RAM和匿名用户 |
Bucket ACL | Bucket |
Object ACL | Object |
RAM Policy为
JSON
格式,其结构为:{
"Version": "1",
"Statement": [
{
"Effect": "Deny",
"Action": [
"oss:PutObject"
],
"Resource": [
"acs:oss:*:*:examplebucket"
],
"Condition": {}
}
]
}
可以使用官方的RAM策略编辑器快速生成RAM策略。Version
:版本Statement
:授权语句Effect
:效力(Allow / Deny)Action
:操作Resource
:资源Condition
:条件
【示例】禁止指定RAM上传Object到Bucket。 step1:生成
RAM Policy
:
{
"Version": "1",
"Statement": [
{
"Effect": "Deny",
"Action": [
"oss:PutObject"
],
"Resource": [
"acs:oss:*:*:avatar"
],
"Condition": {}
}
]
}
step2:添加权限策略: 控制台 - RAM访问控制 - 权限管理 - 创建权限策略 - 脚本编辑
step3:为RAM添加权限策略: 控制台 - RAM访问控制 - 身份管理 - 用户 - 添加权限
Bucket Policy
package mainimport (
"fmt"
"os"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)func Main() {
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 配置policy(禁止192.168.1.1之外的所有人对examplebucket的所有请求)
policy := `{
"Version": "1",
"Statement": [
{
"Principal": ["*"],
"Effect": "Deny",
"Action": ["oss:*"],
"Resource": ["acs:oss:*:*:examplebucket"],
"Condition": {
"NotIpAddress": {
"acs:SourceIp": ["192.168.1.1"]
}
}
}
]
}`// 设置
err = client.SetBucketPolicy("examplebucket", policy)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 查询
policy, e := client.GetBucketPolicy("examplebucket")
if e != nil {
fmt.Println("Error:", e)
os.Exit(-1)
}// 删除
err = client.DeleteBucketPolicy("examplebucket")
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
}
Bucket ACL
package mainimport (
"fmt"
"os"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)func Main() {
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 设置examplebucket的权限为公共读
err = client.SetBucketACL("examplebucket", oss.ACLPublicRead)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 设置examplebucket的权限为公共读写
err = client.SetBucketACL("examplebucket", oss.ACLPublicReadWrite)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 设置examplebucket的权限为私有
err = client.SetBucketACL("examplebucket", oss.ACLPrivate)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 查询Bucket ACL
res, e := client.GetBucketACL("examplebucket")
if e != nil {
fmt.Println("Error:", e)
os.Exit(-1)
}
}
Object ACL
package mainimport (
"fmt"
"os"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)func Main() {
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 生成Bucket实例
bucket, e := client.Bucket("examplebucket")
if e != nil {
fmt.Println("Error:", e)
os.Exit(-1)
}// 设置exampleobject的权限为公共读
err = bucket.SetObjectACL("exampleobject", oss.ACLPublicRead)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 设置exampleobject的权限为公共读写
err = bucket.SetObjectACL("exampleobject", oss.ACLPublicReadWrite)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 设置exampleobject的权限为私有
err = bucket.SetObjectACL("exampleobject", oss.ACLPrivate)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 设置exampleobject的权限为继承Bucket
err = bucket.SetObjectACL("exampleobject", oss.ACLDefault)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 查询Object ACL
res, er := bucket.GetObjectACL("exampleobject")
if er != nil {
fmt.Println("Error:", er)
os.Exit(-1)
}
}
数据容灾 同城冗余
如果把默认的本地冗余比喻为一份数据存在同一机房内的三个不同硬盘中的话,那么同城冗余就是将数据存在同一数据中心的三个不同机房。
从本地冗余到同城冗余,隔离等级由硬盘级上升到机房级。
err = client.CreateBucket(
"examplebucket",
oss.StorageClass(oss.StorageIA),
oss.ACL(oss.ACLPublicRead),
oss.RedundancyType(oss.RedundancyZRS)
)
/*
RedundancyType:冗余类型。
RedundancyLRS=本地冗余
RedundancyZRS=同城冗余
*/
冗余类型仅支持创建Bucket
时指定,一旦创建不支持修改。
跨城冗余跨城冗余就是将数据存放在不同城市的机房。
从同城冗余到跨城冗余,隔离等级由机房级上升到城市级。
package mainimport (
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"os"
)func main(){
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}/*
指定仅同步复制规则创建后新写入的数据
不同步源Bucket的历史数据
且此后将源Bucket执行的所有操作都将实时同步到目标Bucket
*/
putXml := `
prefix_1
prefix_2
ALL
destexamplebucket
oss-cn-beijing
oss_acc
disabled
aliyunramrole
Enabled
c4d49f85-ee30-426b-a5ed-95e9139d****
`// 开启跨区域复制
err = client.PutBucketReplication("srcexamplebucket",putXml)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
}
数据加密 OSS服务侧加密
上传
Object
时,OSS
对收到的文件进行加密,再将加密文件持久化保存。下载
Object
时,OSS
自动将加密文件解密后返回,并在响应头中返回x-oss-server-side-encryption
,用于声明该文件进行了OSS
服务侧加密。package mainimport (
"fmt"
"os""github.com/aliyun/aliyun-oss-go-sdk/oss"
)func main() {
// 创建OSSClient实例
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 初始化加密规则,使用AES256算法
config := oss.ServerEncryptionRule{
SSEDefault: oss.SSEDefaultRule{SSEAlgorithm: "AES256"},
}// 设置OSS服务侧加密
err = client.SetBucketEncryption("examplebucket", config)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
}
应用服务侧加密
应用服务侧加密是指将
Object
发送到OSS
之前进行加密,然后将加密后的Object
传输至OSS
持久化保存。文章图片
package mainimport (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os""github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/aliyun/aliyun-oss-go-sdk/oss/crypto"
)func main() {
// 创建OSSClient实例。
client, err := oss.New(
"", "", "",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 创建一个主密钥的描述信息,创建后不允许修改。主密钥描述信息和主密钥一一对应。
// 如果所有的Object都使用相同的主密钥,主密钥描述信息可以为空,但后续不支持更换主密钥。
// 如果主密钥描述信息为空,解密时无法判断使用的是哪个主密钥。
// 强烈建议为每个主密钥都配置主密钥描述信息(json字符串), 由客户端保存主密钥和描述信息之间的对应关系(服务端不保存两者之间的对应关系)。// 由主密钥描述信息(json字符串)转换的map。
materialDesc := make(map[string]string)
materialDesc["desc"] = ""// 根据主密钥描述信息创建一个主密钥对象。
masterRsaCipher, err := osscrypto.CreateMasterRsa(
materialDesc, "", "",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 根据主密钥对象创建一个用于加密的接口, 使用aes ctr模式加密。
contentProvider := osscrypto.CreateAesCtrCipher(masterRsaCipher)// 获取bucket。
cryptoBucket, err := osscrypto.GetCryptoBucket(client, "", contentProvider)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 上传时时自动加密。
err = cryptoBucket.PutObject(
"", bytes.NewReader([]byte("yourObjectValueByteArray")),
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 下载时自动解密。
body, err := cryptoBucket.GetObject("")
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
defer func(b *io.ReadCloser) {
err = (*b).Close()
if err != nil {
log.Println("Error:", err)
}
}(&body)data, err := ioutil.ReadAll(body)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
fmt.Println("data:", string(data))
}
版本控制 版本控制以回滚至任一历史版本的方式,为错误覆盖和错误删除提供容错空间。
版本控制的一些限制:
- 与合规保留策略无法同时配置
- 开启版本控制的Bucket,
x-oss-forbid-overwrite
请求头将不再生效 - 不建议同时开通OSS-HDFS服务并设置保留策略
package mainimport (
"fmt"
"os""github.com/aliyun/aliyun-oss-go-sdk/oss"
)func main() {
// 创建Client实例。
client, err := oss.New(
"", "", "",
)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 创建Bucket实例。
err = client.CreateBucket("")
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 开启版本控制。
config := oss.VersioningConfig{Status: "Enabled"}
err = client.SetBucketVersioning("", config)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 暂停版本控制。
config.Status = "Suspended"
err = client.SetBucketVersioning("", config)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
}
防盗链
OSS
通过对比request header
的Referer
与配置规则是否一致来判断允许或拒绝该请求。package mainimport (
"fmt"
"os""github.com/aliyun/aliyun-oss-go-sdk/oss"
)func main() {
// 创建Client实例。
client, err := oss.New("yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret")
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 初始化Referer白名单。支持通配符星号(*)和问号(?)。
referers := []string{
"http://www.aliyun.com",
"http://www.???.aliyuncs.com",
"http://www.*.com",
}// 设置Referer白名单,并禁止空Referer
// 注意:如果你配置了白名单但允许空Referer,结局会很僵硬...
err = client.SetBucketReferer("yourBucketName", referers, false)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}// 查询Bucket Referer
res, e := client.GetBucketReferer("yourBucketName")
if e != nil {
fmt.Println("Error:", e)
os.Exit(-1)
}
fmt.Println(res.RefererList, res.AllowEmptyReferer)
}
合规保留策略 合规保留策略允许用户以“不可删除、不可篡改”方式保存和使用数据,符合美国证券交易委员会(SEC)和金融业监管局(FINRA)的合规要求。
package mainimport (
"fmt"
"os""github.com/aliyun/aliyun-oss-go-sdk/oss"
)func main() {
// 创建OSSClient实例。
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
handleError(err)
}// 初始化WORM规则(锁定60天)。
// 最短锁定1天,最长锁定70年。
wormID, e := client.InitiateBucketWorm("examplebucket", 60)
if e != nil {
handleError(e)
}/*
初始化后的24小时内,WORM处于InProgress状态。
在此期间,可以提交锁定,或者删除。
若在此期间即未提交锁定也未删除,则WORM自动失效。
*/// 提交锁定
err = client.CompleteBucketWorm("examplebucket", wormID)
if err != nil {
handleError(err)
}// 或者删除
/*
err = client.AbortBucketWorm("examplebucket")
if err != nil {
handleError(err)
}
*/// 通过Bucket查询WORM
wormConfiguration, er := client.GetBucketWorm("examplebucket")
if er != nil {
handleError(er)
}// WORM一旦提交锁定,在解锁日期前,WORM和Bucket内的所有Object均不支持删除。
// 但可以延长锁定期:
err = client.ExtendBucketWorm(
"examplebucket", 180, wormConfiguration.WormId,
)
if err != nil {
handleError(err)
}
}func handleError(err error) {
fmt.Println("Error:", err)
os.Exit(-1)
}
日志
package mainimport (
"fmt"
"os""github.com/aliyun/aliyun-oss-go-sdk/oss"
)func main() {
// 创建OSSClient实例。
client, err := oss.New(
"yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret",
)
if err != nil {
handleError(err)
}// 开启
// 将examplebucket的日志存入destbucket。
err = client.SetBucketLogging("examplebucket", "destbucket", "log/", true)
if err != nil {
handleError(err)
}// 查询
res, e := client.GetBucketLogging("examplebucket")
if e != nil {
handleError(e)
}
fmt.Println(
res.LoggingEnabled.TargetBucket, res.LoggingEnabled.TargetPrefix,
)// 关闭
err = client.DeleteBucketLogging("examplebucket")
if err != nil {
handleError(err)
}
}func handleError(err error) {
fmt.Println("Error:", err)
os.Exit(-1)
}
细节 防止意外覆盖
默认情况下,如果上传的文件与已有文件同名,则覆盖已有文件。
为防止文件被意外覆盖,可以在上传请求的
Header
中携带参数x-oss-forbid-overwrite
,并指定其值为true
。避免修改路径
OSS
可以通过一些操作来模拟类似文件夹的功能,但是代价非常昂贵。比如重命名目录,希望将性能与扩展性最佳实践test1
目录重命名成test2
,那么OSS
的实际操作是将所有以test1/
开头的Object
都重新复制成以test2/
开头的Object
。
这是一个非常消耗资源的操作。因此在使用OSS
的时候要尽量避免类似的操作。
Object Key不要使用顺序前缀!!!
Object Key不要使用顺序前缀!!!
Object Key不要使用顺序前缀!!!
阿里云 对象存储 OSS
推荐阅读
- 前端面试|JS数组乱序的几种方法
- 微信小程序使用navigator实现页面跳转功能
- 科普达人丨一图看懂阿里云ECS
- Django4.0 RestFramework 序列器使用
- 官宣,首届阿里云NoSQL数据库峰会来袭!火速收藏
- vue项目打包后使用reverse-sourcemap反编译到源码(详解版)
- Flutter学习第四课(SharedPreferences本地存储的简单使用)
- html网页设计|使用html+css+js实现一个静态页面(含源码)
- html期末大作业|学生HTML个人网页作业作品 使用HTML+CSS+JavaScript个人介绍博客网站 web前端课程设计 web前端课程设计代码 web课程设计
- 使用|使用 Presto 和 Alluxio 在 AWS 上搭建高性能平台来支持实时游戏服务