在开发一个应用,也就是我们俗称 App 时,最低的配置是需要一个前端和一个后端。由前端技术人员为用户开发接触到的页面,由后端为前端的各类用户事件提供处理和数据响应。比较常见的,如手机 App 应用(QQ、微信),网页 Web 应用(GitChat、CSDN 页面),桌面应用(YY、QQ 游戏)…… 不管是什么应用,都需要有后端技术与之匹配。而我们今天介绍的,就是其中的 Web 后端技术及相关实战。
由于 Chat 篇幅不适合描述过多实战无关的细节,对一些需要深入了解的知识,作者会贴上一些链接给读者们参阅。
思维导图
代码托管仓库:
https://github.com/fwhezfwhez/gitchat
可操作多核,实现高负载
Golang 可以使用 Go 关键字轻松操作多协程,并指定 CPU 数。
runtime.GOMAXPROCS(runtime.NumCPU())
go func(){}()
可以跨平台编译,比如在 Windows 上开发,交叉编译成 Linux 的可执行文件。
编程友好
Golang 易学、性能好,并且它是为了降低 C++/C 开发维护复杂度而诞生的新型语言,处理业务相当简单。上手成本几乎是零(不需要了解 Spring 全家桶)。
大部分中间件(Redis、Mongo、Consul、etcd……) 服务仅仅只需要相应的 IP 和端口,没有严格意义上要求本机安装,而正好 Docker 提供隔离级别良好的镜像服务,可以达到本机安装的效果。Gopher 的开发者大部分使用 Windows 和 Mac,今天也是拿 Win10 举例。正确安装好 Go 和 Docker 后,需要达到一下效果:
go --version
docker --version
顺便贴一下,具备 Docker 环境下,如何准备 PostgreS、Redis 和 Mongo。
docker pull postgres:9.6
docker pull redis:latest
docker pull mongo:latest
开启(长期) 以上三个中间件服务:
**因为本机已包含 PostgreS,5432 已经被占用,使用本机 5433 映射进 docker5405);margin:0px 0px 1.1em;font-family:“Source Code Pro”, monospace;font-size:0.9em;text-align:start;padding:10px 20px;border:0px;border-radius:5px;outline:0px;">
docker run -itd --restart=on-failure:20 -p 6379:6379 redis:latest
docker run -itd --restart=on-failure:20 -p 5433:5432 postgres:9.6
docker run -itd --restart=on-failure:20 -p 27017:27017 mongo:latest
**
**
为了让接下来的实战场景通俗易懂,需要为读者介绍一些概念。这些概念根据其定义并结合了作者的认知而得出,如果和大家的一些理念相悖,可以在作者的读者圈提出自己的看法。技术的领域里,希望能抱着求同存异的心态来对待分歧。
浏览器与服务器之间是基于 HTTP(S) 的应答式通信,那么 HTTP 请求与响应长啥样呢,不妨按照如下操作看看(包括但不限于在 360 浏览器/Chrome 浏览器中尝试):
POST or:rgb(188, 96, 96);outline:0px;"\>/user/ 获取用户列表
GET /user/1/ 获取id为1的用户记录
PATCH /user/1/ 修改id为1的用户的属性
DELETE /user/1/ 删除id为1的用户
RESTful 的路径里,没有下划线和驼峰,没有纯动词,需要连接时,使用 -
:
/user/add-money/
/user/order/amount/
HTTP 请求的结构序列化方式,大部分是 application/json、application/xml、application/x-www-form-urlencoded,其中 JSON 使用的最多,三者都是文本协议,JSON 大体长这样:
{
"chat\_name":"golang gitchat",
"readers": [
{
"username":"张三"
},
{
"username":"李四"
}
]
}
Gin 是 Golang 实现的 Web 框架,上手难度极低,使用时贴合了 RESTful 习惯。
仓库坐标:
https://github.com/gin-gonic/gin
对一个陌生的框架,我们不要求全部掌握,但可以通过功能点,来学习如何使用。
如何搭建 RESTful 样式的 HTTP 服务,监听 POST/GET/PATCH/DELETE 请求
restful.go
package main
import (
"github.com/gin-gonic/gin"
"strconv"
)
type User struct {
Id int `json:"id"`
Username string `json:"username"`
}
var users = []User{{1, "张三"}, {2, "李四"}, {3, "王五"}}
func main() {
r := gin.Default()
r.GET("/user/", get)
r.GET("/user/:id/", getOne)
r.POST("/user/", post)
r.PATCH("/user/:id/", patch)
r.DELETE("/user/:id/", delete)
r.Run(":8080")
}
func get(c *gin.Context) {
c.JSON(200, users)
}
func post(c *gin.Context) {
var user User
c.Bind(&user)
users = append(users, User{Username: user.Username, Id: user.Id})
c.JSON(200, users)
}
func patch(c *gin.Context) {
var user User
c.Bind(&user)
id := c.Param("id")
for i,v:=range users{
if strconv.Itoa(v.Id) == id {
users[i].Username = user.Username
user.Id = v.Id
}
}
c.JSON(200, user)
}
func deleteById(c *gin.Context) {
id := c.Param("id")
for i,v:=range users{
if strconv.Itoa(v.Id) == id {
users = append(users[:i], users[i+1:]...)
}
}
c.JSON(200, users)
}
func getOne(c *gin.Context) {
var user User
id := c.Param("id")
for _,v:=range users{
if strconv.Itoa(v.Id) == id {
user = v
break
}
}
c.JSON(200, user)
}
go run restful.go
如何使用中间件
使用中间件验证身份(是否带 token)
middleware.go
go run middleware.go
PostgreS 是一款免费的功能强大的开源数据库,端口号为 5432,安装时可以选择本机安装,也可以使用 Docker。这里使用 Docker。
在前面,我们已经通过 Docker 安装了 PostgreS,并且运行了,那么我要如何操作呢?
关系型数据库基本上都是通过 SQL 语句来完成数据的增删改查,具体的概念,可以参考:
关于 PostgreS、Redis、Mongo,数据存哪里的问题:
[
三者里,PostgreS 有事务支持,并且读写速度都很好,唯一的缺陷是和众多关系数据库一样,连接数的上限存在瓶颈,它是库表列形式的关系型数据库,可以通过规范的 SQL 语句操作。
Redis 和 Mongo 都是无事务的非关系型数据库,前者有众多模型(队列、键值、管道等),后者以集合、文档、字段的形式存放 JSON 字段。
Redis 和 Mongo 都是内存数据库,读写效率都比 PostgreS 快,前者在应用中经常作为缓存,降低数据库连接开销,后者可以存放弱事务的记录(日志、社区论坛的各种数据……)
现在,我们尝试将操作的对象 users 从程序内存中,转储进 PostgreS。以下为涉及到的命令语句,使用场景都凝结进 GIF 了。
进入 PostgreS 终端:
docker exec -it
sh 以超管 PostgreS 进入 PostgreS 数据库命令行:
psql -U postgres -d postgres
创建 gitchat 用户,密码 123456:
create user gitchat with password '123456'
创建 test 数据库,用来存放我们的数据表:
create database test
将测试数据库 test 所有权限赋予用户 gitchat:
grant all privileges on database test to gitchat
退出命令行:
\q
以 gitchat 账户进入 test 账户:
psql -U gitchat -d test
create table user_info( id serial primary key, username varchar not null default '' )
](https://www.runoob.com/postgresql/postgresql-tutorial.html)
restful-pg.go
package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/lib/pq" "time" ) type User struct { Id int `json:"id"` Username string `json:"username"` } var db *gorm.DB func init() { var err error db, err = gorm.Open("postgres", fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=%s password=%s", "localhost", "5433", "gitchat", "test", "disable", "123456", ), ) db.SingularTable(true) db.LogMode(true) db.DB().SetConnMaxLifetime(10 * time.Second) db.DB().SetMaxIdleConns(30) if err != nil { panic(err) } } func main() { r := gin.Default() r.GET("/user/", get) r.GET("/user/:id/", getOne) r.POST("/user/", post) r.PATCH("/user/:id/", patch) r.DELETE("/user/:id/[](resources/)nd(&user);e!=nil{ panic(e) } if e := db.Raw("insert into user_info(username) values(?) returning *", user.Username).Scan(&user).Error; e != nil { c.JSON(500, gin.H{"message": e.Error()}) return } c.JSON(200, user) } func patch(c *gin.Context) { var user User c.Bind(&user) id := c.Param("id") db.Raw("update user_info set username=? where id=? returning *", user.Username, id).Scan(&user) c.JSON(200, user) } func deleteById(c *gin.Context) { id := c.Param("id") db.Exec("delete from user_info where id=?", id) c.JSON(200, gin.H{"message": "success"}) } func getOne(c *gin.Context) { var user User id := c.Param("id") db.Raw("select * from user_info where id=?", id).Scan(&user) c.JSON(200, user) }
go run restful-pg.go
2.7 Redis
Redis 是一款非关系型数据库,具备多种业务模型。前面我们通过 Docker 已经启动过了 Redis,并将主机 6379 端口和 Redis 容器 6379 映射到了一起。
Redis 的主要业务场景:为查询提供缓存,跨服务通信数据传递。
后者我们不做演示,其原理是两个服务之间不进行收发,当需要通信时,一个服务把需要传递的消息以 Key-Value 的形式放入 Redis,另一个服务去取。Redis 的各种命令我们不去深入,这里用到了 SETEX 和 GET 来进行数据缓存的测试。
连接 Redis 需要有对应语言的客户端,下面我们测试 Go 客户端,并为上面的 user 查询制定缓存。
redis-example.go
将数据库 user_info 表按照 id 缓存进 Redis
restful-redis-pg.go
package main import ( "encoding/json" "fmt" "github.com/garyburd/redigo/redis" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/lib/pq" "time" ) type User struct { Id int `json:"id"` Username string `json:"username"` } var pool = getRedis("redis://localhost:6379") func (u *User) SyncRedis(conn redis.Conn) { if conn == nil { conn = pool.Get() defer conn.Close() } buf, _ := json.Marshal(u) key := fmt.Sprintf("gitchat:user_info:%d", u.Id) _, e := conn.Do("SETEX", key, 60*60*24, buf) if e != nil { panic(e) } } var db *gorm.DB func init() { var err error db, err = gorm.Open("postgres", fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=%s password=%s", "localhost", "5433", "gitchat", "test", "disable", "123456", ), ) db.SingularTable(true) db.LogMode(true) db.DB().SetConnMaxLifetime(10 * time.Second) db.DB().SetMaxIdleConns(30) if err != nil { [](resources/)ser_info").Scan(&users).Error; e != nil { c.JSON(500, gin.H{"message": e.Error()}) return } conn := pool.Get() defer conn.Close() for i, _ := range users { users[i].SyncRedis(conn) } c.JSON(200, users) } func post(c *gin.Context) { var user User if e := c.Bind(&user); e != nil { panic(e) } if e := db.Raw("insert into user_info(username) values(?) returning *", user.Username).Scan(&user).Error; e != nil { c.JSON(500, gin.H{"message": e.Error()}) return } user.SyncRedis(nil) c.JSON(200, user) } func patch(c *gin.Context) { var user User c.Bind(&user) id := c.Param("id") db.Raw("update user_info set username=? where id=? returning *", user.Username, id).Scan(&user) user.SyncRedis(nil) c.JSON(200, user) } func deleteById(c *gin.Context) { id := c.Param("id") db.Exec("delete from user_info where id=?", id) c.JSON(200, gin.H{"message": "success"}) } func getOne(c *gin.Context) { var user User id := c.Param("id") conn :=pool.Get() defer conn.Close() buf,e :=redis.Bytes(conn.Do("GET", fmt.Sprintf("gitchat:user_info:%s", id))) if e!=nil { panic(e) } if len(buf) !=0 { e= json.Unmarshal(buf, &user) if e!=nil { panic(e) } } else { db.Raw("select * from user_info where id=?", id).Scan(&user) } c.JSON(200, user) } func getRedis(url string) *redis.Pool { return &redis.Pool{ MaxIdle: 200, //MaxActive: 0, IdleTimeout: 180 * time.Second, Dial: func() (redis.Conn, error) { c, err := redis.DialURL(url) if err != nil { fmt.Println(err) return nil, err } return c, err }, TestOnBorrow: func(c redis.Conn, t time.Time) error { _, err := c.Do("PING") return err }, } }
缓存以前
缓存之后
调用
GET localhost:8082/user/1/
时, 已经不再存在数据库连接和 SQL 语句了, 并且平均查询速度也更快了。2.8 Mongo
Mongo 和 Redis 一样,是内存数据库,对高并发的支持比 Redis 可用性更高,因为 Redis 是单线程的。
Mongo 的业务场景面向一些量级很大(未来可能会变得很大)的数据,下面我们在前面的 restful-redis-pg 的基础上,增加一个访问日志记录,这些日志被记入 Mongo。
Mongo 除了快以外,还有一个好处,如果 DB 和 Collection 不存在时,会自动创建。
restful-redis-mongo-pg.go
package main import ( "bytes" "encoding/json" "fmt" "github.com/garyburd/redigo/redis" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/lib/pq" "gopkg.in/mgo.v2" "io/ioutil" "time" ) type User struct { [](resources/)Id) _, e := conn.Do("SETEX", key, 60*60*24, buf) if e != nil { panic(e) } } var db *gorm.DB var mgoSession *mgo.Session var col *mgo.Collection func init() { var err error db, err = gorm.Open("postgres", fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=%s password=%s", "localhost", "5433", "gitchat", "test", "disable", "123456", ), ) db.SingularTable(true) db.LogMode(true) db.DB().SetConnMaxLifetime(10 * time.Second) db.DB().SetMaxIdleConns(30) if err != nil { panic(err) } mgoSession, err = mgo.Dial("localhost:27017") if err != nil { panic(err) } // Optional. Switch the session to a monotonic behavior. mgoSession.SetMode(mgo.Monotonic, true) col = mgoSession.DB("test").C("user_info") } func clear() { mgoSession.Close() db.Close() } type VisitLog struct { URL string `json:"url"` IP string `json:"ip"` ContentType string `json:"content_type"` Body []byte `json:"body"` Query string `json:"query"` CreatedAt time.Time `json:"created_at"` } func VisitLogMiddleware(c *gin.Context) { defer c.Next() var vl VisitLog vl.URL = c.Request.URL.String() vl.ContentType = c.ContentType() vl.IP = c.ClientIP() buf, _ := ioutil.ReadAll(c.Request.Body) if len(buf) != 0 { vl.Body = buf c.Request.Body = ioutil.NopCloser(bytes.NewReader(buf)) } vl.Query = c.Request.URL.Query().Encode() vl.CreatedAt = time.Now() err := col.Insert(&vl) if err != nil { panic(err) } } func main() { defer clear() r := gin.Default() r.Use(VisitLogMiddleware) r.GET("/user/", get) r.GET("/user/:id/", getOne) r.POST("/user/", post) r.PATCH("/user/:id/", patch) r.DELETE("/user/:id/", deleteById) r.GET("/visit-log/", visitLogControl) r.Run(":8082") } func get(c *gin.Context) { var users []User if e := db.Raw("select * from user_info").Scan(&users).Error; e != nil { c.JSON(500, gin.H{"message": e.Error()}) return } conn := pool.Get() defer conn.Close() for i, _ := range users { users[i].SyncRedis(conn) } c.JSON(200, users) } func post(c *gin.Context) { var user User if e := c.Bind(&user); e != nil { panic(e) } if e := db.Raw("insert into user_info(username) values(?) returning *", user.Username).Scan(&user).Error; e != nil { c.JSON(500, gin.H{"message": e.Error()}) return } user.SyncRedis(nil) c.JSON(200, user) } func patch(c *gin.Context) { var user User c.Bind(&user) id := c.Param("id") db.Raw("update user_info set username=? where id=? returning *", user.Username, id).Scan(&user) user.SyncRedis(nil) c.JSON(200, user) } func deleteById(c *gin.Context) { id := c.Param("id") db.Exec("delete from user_info where id=?", id) c.JSON(200, gin.H{"message": "success"}) } func getOne(c *gin.Context) { var user User id := c.Param("id") conn := pool.Get() defer conn.Close() buf, e := redis.Bytes(conn.Do("GET", fmt.Sprintf("gitchat:user_info:%s", id))) if e != nil { panic(e) } if len(buf) != 0 { e = json.Unmarshal(buf, &user) if e != nil { panic(e) } } else { db.Raw("select * from user_info where id=?", id).Scan(&user) } c.JSON(200, user) } func getRedis(url string) *redis.Pool { return &redis.Pool{ MaxIdle: 200, //MaxActive: 0, IdleTimeout: 180 * time.Second, Dial: func() (redis.Conn, error) { c, err := redis.DialURL(url) if err != nil { fmt.Println(err) return nil, err } return c, err }, TestOnBorrow: func(c redis.Conn, t time.Time) error { _, err := c.Do("PING") return err }, } } func visitLogControl(c *gin.Context) { var results []VisitLog err := col.Find(nil).All(&results) if err != nil { c.JSON(500, gin.H{"message": err.Error()}) panic(err) } c.JSON(200, results) }
3. 实例
仓库:https://github.com/fwhezfwhez/gitchat
game 项目提供了简要的游戏后端架构模型,根据业务的集中和分布性,重新拆分成了(最少 3 个) 子 Broker,分别为 app-center、backend、activities。
共享出来的仅仅是这个后端项目的基础架构,并没有把所有的核心代码放进来,但这一部分代码也足以给我们提供启发。
3.1 app-center
app-center 是该后端项目里的核心 Broker,应对来自所有客户端的请求。它提供了以下四种协议供不同的游戏客户端选择,可以适用大多数的业务场景。
TCP
srv := tcpx.NewTcpX(tcpx.ProtobufMarshaller{}) srv.HeartBeatMode(true, 10*time.Second) srv.AddHandler(1, func(c *tcpx.Context) { // HeartBeat c.RecvHeartBeat() }) fmt.Println("tcp listens on 7001") _ = srv.ListenAnd[](resources/)r:transparent;text-size-adjust:none;-webkit-font-smoothing:antialiased;background:0px 0px;box-sizing:border-box;transition:background-color 0.15s ease-in-out 0s, color 0.15s ease-in-out 0s, border-color 0.15s ease-in-out 0s;color:rgb(79, 161, 219);text-decoration:none;outline:0px;">https://github.com/fwhezfwhez/tcpx,解决了游戏内即聊、数据对发、服务端通知等场景。 上例为部分代码,TCP 连接使用的序列化协议为 Protobuf。 Protobuf 有别于 JSON,它是一个二进制协议,具备极高的传输效率以及很小的体积。并且,Protobuf 随着 gRPC 的普及,几乎为所有的客户端/服务端语言支持。 和基本的 TCP 使用相比,TCPX 解决了粘包、易用、路由、中间件、心跳等常见问题。 相关了解: * [如何使用 Protobuf 序列化结构](https://blog.csdn.net/fwhezfwhez/article/details/92740978) * [如何在 Golang 中使用 TCP](https://github.com/fwhezfwhez/TestX/tree/master/test_tcp/basic) HTTP
r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) // prop propRouter.HTTPRouter(r) // activity activityRouter.HTTPRouter(r) s := &http.Server{ Addr: "8001", Handler: cors.AllowAll().Handler(r), ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 21, } fmt.Println("http listens on 8001") s.ListenAndServe()
app-center 里会对一些快速上线的需求使用 HTTP 协议,因为 HTTP 的开发效率是极快的, 尤其是和第三方或者其他团队接入的时候。因为 TCP 在解析时,需要处理粘包、拆包,提高了对接上的协议复杂度,而 HTTP 的协议约定几乎不需要磨合,统一使用 application/json,少数场景会使用 Form 和 XML。 比如,在对接 HTTP 时,前后端只需要协定:
{
“username”: “gitchat”
}而对接 TCP 时,最少需要协定:
[4]byte length
[]byte payload而根据组包的预设来看,有的协议会配置成诸如:
[4]byte length
[4]byte messageID
[4]byte headerLength
[4]byte bodyLength
[]byte header
[]byte body这样复杂的样式。 gRPC
lis, err := net.Listen("tcp", ":6001") if err != nil { fmt.Println(err.Error()) return } s := grpc.NewServer() // prop propPb.RegisterPropServiceServer(s, &propService.PropService{}) fmt.Println("grpc listens on 6001") s.Serve(lis)
gRPC 在内部服务间的使用十分频繁,本身并不是暴露给外界用的。可以把 gRPC 理解成 HTTP 和 TCP 的集成,统筹了二者的优势,既有 TCP 的效率,又和 HTTP 一样高度统一。统一体现在 Proto 文件上,协议的内部细节不像 TCP 一样需要很长的对接期(当然,Stream 模式的 gRPC,据说也是要拆组包滴)。 在服务中心,gRPC 为 Broker 之间的服务调用,起了很好的疏导作用。比如 **Broker** activity 就是通过 gRPC 来调用 app-center 中的“赠送道具”接口的。 因为活动是以插件的形式开发,新增一个活动,大概率会包给其他的团队,不会直接给他们提供数据库的操作权,而是通过把服务通过 gRPC 外放给他们用。 KCP 这部分代码其实在库中注释掉了,实际的游戏客户端也没有介入。 KCP 的载速是 TCP 的 1.3 到 1.5 倍,代价是一定程度的带宽。项目里原本考虑用 KCP 供给用户选择做提速方案,可是因为本来就很快了,没有速度上的瓶颈,所以才暂时没有启用! 但 KCP 在代码上确实是没有成本的,在 TCPX 里集成了如下:
srv := tcpx.NewTcpX(tcpx.ProtobufMarshaller{}) srv.HeartBeatMode(true, 10*time.Second) srv.AddHandler(1, func(c *tcpx.Context) { // HeartBeat c.RecvHeartBeat() }) + go func() { + fmt.Println("kcp listens on 7002") + _ = srv.ListenAndServe("kcp","7002") + }() fmt.Println("tcp listens on 7001") _ = srv.ListenAndServe("tcp", ":7001")
7002 的 KCP 和 7001 的 TCP,共享了服务路由,所有进入 TCP 的请求,只需要接入 KCP 里,就能达到完全一样的业务效果,牺牲一部分带宽来提升速度,WiFi 中有惊喜。 #### 3.2 backend backend 是很传统的 HTTP 后端业务。在刚开始时,服务仅有一个 app-center,业务和后端共用了一个服务。 可随之而来有个问题,后端的代码经常变动,影响到了游戏业务中的游戏体验。 为了解决**后端业务经常更新,造成服务重启频繁**问题,将它从 app-center 中分离是很有必要的。 和 app-center 相比,HTTP 的业务,在验证方式上也有不同。 backend HTTP 后端需要有登陆和人工管理,使用的是 JWT。 app-center HTTP 后端提供客户端调用,不存在传统的 JWT 场景,大部分是通过 `hash(app-secret, salt)` 来校验。 JWT
func JWTValidate(c *gin.Context) {
token := c.Request.Header.Get(“Authorization”)
if token == “” {
c.JSON(402, gin.H{“message”: “valid fail”})
c.Abort()
return
}tk, info := jwt_util.JwtTool.ValidateJWT(token) if !tk.Valid { c.JSON(402, gin.H{"message": info}) c.Abort() return } r := tk.Claims.(jwt.MapClaims) c.Set("user_id", r["user_id"])
}
r.POST("/login/", genToken) r.Use(middleware.JWTValidate) // activity activityRouter.Router(r) // prop propRouter.HTTPRouter(r)
后台业务,可以说是后端里最基本了,因为仅仅使用 HTTP 协议,涉及到的概念,几乎只有几个标签 RESTful、Gin、PostgreS,是标准的 CRUD 业务领域。 #### 3.3 activity activity 和 backend 一样,原本是 app-center 中的一个子模块,但和其他模块相比,它有一个很显著的特性:**经常更新,维护团队相对更大。** 和 backend 相比,游戏的活动变动更加剧烈: * 新的活动推广,需要根据不同的统计需求,在各个地方埋点,每一次埋点,都是一次变动,需要更新; * 每一个活动都需要运营关注,在多个业务联动的活动场景里,团队的组成远比后台复杂; * 活动引流是极高的负载点之一,需要均衡。 上述列表的每一个点都说明了一件事,把 activity 单独拉出来做服务十分必要。单独拉子服务,可以有下列好处: * 子业务更新,可以不对主业务重启。活动在上架、关闭、更新时,activity 不会要求 app-center 重启。 * 子业务可以由不同的团队单独运营。比方说,需要在两款游戏之间联动,斗地主和麻将集中推出合作业务,这样的活动可以临时开辟团队开发和维护。 * 方便均衡。收缩的子业务,可以小巧轻便地部署在多个服务器,也可以很方便做负载均衡,几乎可以算是微服务了,但是粒度上会更大一点。 这次的实践里,主要是以插件化的形式,开发一个子活动——邀请有礼。 整个开发的模块,只在 game/brokers/activities/inviteActivity 里,backend 和 app-center 作为预设,提供了活动的 CRUD、道具的 CRUD,以及一些关键的 gRPC 接口,[项目](https://github.com/fwhezfwhez/gitchat/tree/master/chat1-web%E5%90%8E%E7%AB%AF%E5%AE%9E%E6%88%98/project)的文档和指引正在慢慢补全。 **邀请活动的要求**: * 邀请方为甲方,被邀请方为乙方; * 乙方应邀进入游戏时,赠送甲方一枚“人缘好的证明”,每天最多赠送 5 枚; * 每天凌晨 4 点重置甲方进度; * 每个星期统计上周人缘好的证明最多的玩家前十名,并发放“人气少年王”。 **登陆活动的要求**: * 登陆时,赠送“今日的太阳”一枚。 邀请活动在库里已经完成了,登陆活动留下来练手。
欢迎关注我的公众号,回复关键字“Golang” ,将会有大礼相送!!! 祝各位面试成功!!!