├── .gitignore
├── LICENSE
├── README.md
├── app
├── apiCall.go
├── apnsmsg.go
├── app.go
├── appWeb.go
├── application.go
├── client.go
├── config.go
├── contact.go
├── device.go
├── fileLink.go
├── org.go
├── push.go
├── pushCnt.go
├── qun.go
├── resource.go
├── ret.go
├── session.go
├── shieldMsg.go
└── token.go
├── appweb
├── conf
│ └── app.conf
├── controllers
│ ├── index.go
│ ├── ret.go
│ └── user.go
├── main.go
├── models
│ ├── org.go
│ ├── qun.go
│ └── user.go
├── routers
│ └── router.go
├── setting
│ └── setting.go
├── static
│ ├── css
│ │ ├── bootstrap-theme.css
│ │ ├── bootstrap-theme.css.map
│ │ ├── bootstrap-theme.min.css
│ │ ├── bootstrap.css
│ │ ├── bootstrap.css.map
│ │ ├── bootstrap.min.css
│ │ └── login.css
│ ├── fonts
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.svg
│ │ ├── glyphicons-halflings-regular.ttf
│ │ └── glyphicons-halflings-regular.woff
│ ├── images
│ │ ├── bg-bottom.png
│ │ ├── bg-top.png
│ │ ├── loginForm.png
│ │ ├── loginb.png
│ │ ├── logo.png
│ │ ├── one4.jpg
│ │ ├── one5.jpg
│ │ └── selectOrg.png
│ └── js
│ │ ├── bootstrap.js
│ │ ├── bootstrap.min.js
│ │ ├── jquery-2.1.1.min.js
│ │ ├── msg-js.js
│ │ └── npm.js
└── views
│ ├── base
│ ├── base.html
│ ├── base_common.html
│ ├── foot.html
│ └── head.html
│ ├── index.html
│ └── login.html
├── client
└── js
│ ├── README.md
│ ├── demo
│ ├── main.go
│ ├── static
│ │ ├── jquery-1.11.1.min.js
│ │ ├── msg-js.js
│ │ └── ws-flash
│ │ │ ├── WebSocketMain.swf
│ │ │ ├── swfobject.js
│ │ │ └── web_socket.js
│ └── template
│ │ └── demo.html
│ └── flash-security-policy-server
│ ├── main.go
│ └── socketpolicy.xml
├── comet.ini
├── comet
├── channel.go
├── comet.conf
├── config.go
├── db.conf
├── main.go
├── pubsub.go
├── pubsub_tcp.go
├── pubsub_websocket.go
├── rpc.go
├── seq_channel.go
├── signal.go
├── stat.go
├── token.go
└── zk.go
├── db
├── config.go
├── db.conf
└── db.go
├── dependencies.sh
├── hash
├── ketama.go
├── ketama_test.go
├── mmhash3.go
└── mmhash3_test.go
├── hlist
├── hlist.go
└── hlist_test.go
├── id
├── timeid.go
└── timeid_test.go
├── ketama
└── ketama.go
├── message.ini
├── message
├── config.go
├── main.go
├── message-example.conf
├── message.conf
├── mysql.go
├── redis.go
├── rpc.go
├── signal.go
├── storage.go
└── zk.go
├── perf
└── perf.go
├── process
├── process.go
└── process_test.go
├── router
└── cn.go
├── rpc
├── comet.go
├── message.go
├── rand_lb.go
├── ret.go
└── zk.go
├── session
└── session.go
├── sql
├── gopush-cluster.sql
└── install.sql
├── start_all.sh
├── stop_all.sh
├── test
├── comet
│ ├── comet-test-exmaple.conf
│ ├── config.go
│ └── main.go
└── load
│ ├── tcpclient.go
│ └── websocket.go
├── ver
└── ver.go
├── web.ini
├── web
├── admin.go
├── db.conf
├── handle.go
├── handle_1.0.go
├── http.go
├── main.go
├── router.go
├── signal.go
├── static
│ ├── css
│ │ └── user.css
│ └── images
│ │ ├── head-photo-big.png
│ │ └── line-big.png
├── view
│ └── erweima.html
├── web.conf
└── zk.go
├── wiki
├── architecture
│ └── architecture.jpg
├── comet
│ ├── client_proto_zh.png
│ ├── client_proto_zh.textile
│ └── rpc_proto_zh.textile
├── message
│ ├── rpc_proto_en.textile
│ └── rpc_proto_zh.textile
└── web
│ ├── external_proto_en.textile
│ ├── external_proto_zh.textile
│ ├── internal_proto_en.textile
│ └── internal_proto_zh.textile
└── zk
├── zk.go
└── zk_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 |
24 | /web/web
25 | /web/main
26 | /message/message
27 | /message/main
28 | /comet/comet
29 | /comet/main
30 |
31 | /client/java/gopush-cli-java-master/target
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 应用消息服务 [](https://drone.io/github.com/EPICPaaS/appmsgsrv/latest)
2 |
3 | 在 [gopush-cluster](https://github.com/Terry-Mao/gopush-cluster) 基础上做了扩展,加入了用户、群、组织机构等应用元素。
4 |
5 | ## 特性
6 |
7 | * 会话推送
8 | * 用户-用户、用户-群、应用-用户推送
9 | * iOS 通知
10 | * Web 推送
11 | * 图片、语音、链接推送
12 | * 配额与统计
13 |
14 | ## 实现概要
15 |
16 | * 在 gopush 中进行实现(web 进程)
17 | * 用户登录后返回
18 | * id:作为 gopush 的 key
19 | * token:用于身份验证
20 | * 用户/应用/群/组织机构都是使用 id 作为 key
21 | * 用户未登录时使用默认的 key
22 | * 用户发送群组消息时转为对该群组内所有用户的 id 批量发送
23 | * 对外提供对应用数据管理的 RESTful 接口,例如用户 CRUD
24 |
25 | ### 数据库表
26 |
27 | * 用户(user、user_user)
28 | * 组织机构(tenant、org、org_user)
29 | * 应用(application)
30 | * 群(qun、qun_user)
31 | * 客户端版本(client_version)
32 |
33 | ### 会话
34 |
35 | 一个会话对应一个推送连接,连接断开会话终止。
36 |
37 | 可以针对会话级别进行推送控制,请参考[会话推送](https://github.com/EPICPaaS/appmsgsrv/issues/1)。
38 |
39 | ### Name
40 |
41 | 使用 `Name` 作为 gopush-cluster 的 key。
42 |
43 | * 监听:`会话 id` + `@后缀` (监听客户端知道自己的会话)
44 | * 发送:`会话 id` + `@后缀` (推送时服务端根据参数 `sessions` 拼装会话 id)
45 | * 获取离线消息:`uid` + `@后缀`
46 |
47 | #### @后缀
48 |
49 | * 组织机构单位:@tenant
50 | * 组织机构部门:@org
51 | * 群:@qun
52 | * 用户:@user
53 | * 应用:@app
54 |
55 | 开发中可以使用定义在 `app/config.go` 中的常量。
56 |
57 | ## 开发
58 |
59 | * 熟悉 gopush-cluster 的[架构](https://camo.githubusercontent.com/3c2f6df17ff0bace9f88e657819160f0bcb14a8c/687474703a2f2f7261772e6769746875622e636f6d2f54657272792d4d616f2f676f707573682d636c75737465722f6d61737465722f77696b692f6172636869746563747572652f6172636869746563747572652e6a7067)
60 | * 项目只能在 Linux 下开发(通过虚拟机方式方便一些)
61 |
62 | ### web 模块
63 |
64 | 迁出项目后需要修改 web.conf,用于在当前目录下运行的默认配置文件。
65 | * base.app.bind:应用接口监听端口
66 | * zookeeper.addr:gopush-cluster 集群节点通知
67 | * user daniel:当前操作系统登录用户
68 | * 其他默认 IP 绑定
69 |
70 | 启动命令 `./web -v=3 -logtostderr=true`
71 |
72 | ### Postman
73 |
74 | Postman 是一个 Chrome 扩展,用于开发时调试 HTTP 接口。
75 |
76 | * 在线[安装](https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm)(**需翻墙**) / 离线[安装](https://github.com/a85/POSTMan-Chrome-Extension)
77 | * 导入[测试用例](https://www.getpostman.com/collections/cba11454feb866c965c3)
78 | * 修改测试用例中的 IP,开始测试
79 |
80 | ### GitBook
81 |
82 | * [《使用 GitBook 写文档》](http://88250.b3log.org/write-doc-via-gitbook)
83 | * **开发的同时我们也需要同步完善文档**
84 | * https://github.com/EPICPaaS/youxin-dev-guide
85 | * [《有信开发指南》](http://epicpaas.gitbooks.io/youxin-dev-guide/)
86 |
87 | ## 部署
88 |
89 | * static、view 需要放到 bin 目录下
90 |
--------------------------------------------------------------------------------
/app/apnsmsg.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/EPICPaaS/appmsgsrv/db"
7 | //"github.com/EPICPaaS/go-uuid/uuid"
8 | )
9 |
10 | const (
11 |
12 | // 租户资源插入 SQL.
13 | InsertApnsMsgSQL = "INSERT INTO `apns_msg` (`id`, `type`, `apns_token`, `cnt`, `tenant_id`, `pushed`, `created`, `updated`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
14 | // 根据资源id 获取资源
15 | SelectApnsMsgByIdSQL = "SELECT * FROM `apns_msg` where `id` = ?"
16 | // 根据租户 id 查询租户的资源.
17 | SelectApnsMsgByTenantIdSQL = "SELECT * FROM `apns_msg` where `tenant_id` = ?"
18 | // 根据apns_token 查询租户的资源.
19 | SelectApnsMsgByApnsTokenSQL = "SELECT * FROM `apns_msg` where `apns_token` = ?"
20 | //根据id修改资源
21 | UpdateApnsMsgByIdSQL = "UPDATE `apns_msg` SET `type` = ? ,`apns_token` = ? , `cnt` = ? , `tenant_id` = ? ,`pushed` = ? , `created` = ? , `updated` = ? WHERE `id` = ?"
22 | //根据id删除资源
23 | DelApnsMsgByIdSQL = "DELETE FROM `apns_msg` WHERE `id` =? "
24 | //根据tenant_id删除资源
25 | DelApnsMsgByTenantIdSQL = "DELETE FROM `apns_msg` where `tenant_id` = ?"
26 | )
27 |
28 | /**
29 | APNS消息
30 | **/
31 | type ApnsMsg struct {
32 | Id string `json:"id"`
33 | Type string `json:"type"`
34 | ApnsToken string `json:"apns_token"`
35 | Cnt int `json:"cnt"`
36 | TenantId string `json:"tenant_id"`
37 | Pushed string `json:"pushed"`
38 | Created time.Time `json:"created"`
39 | Updated time.Time `json:"updated"`
40 | }
41 |
42 | // 在数据库中查询资源.
43 | func GetApnsMsgById(id string) (*ApnsMsg, error) {
44 | row := db.MySQL.QueryRow(SelectApnsMsgByIdSQL, id)
45 |
46 | apnsmsg := ApnsMsg{}
47 | if err := row.Scan(&apnsmsg.Id, &apnsmsg.Type, &apnsmsg.ApnsToken, &apnsmsg.Cnt, &apnsmsg.TenantId, &apnsmsg.Pushed, &apnsmsg.Created, &apnsmsg.Updated); err != nil {
48 | logger.Error(err)
49 |
50 | return nil, err
51 | }
52 |
53 | return &apnsmsg, nil
54 | }
55 |
56 | // 在数据库中查询资源.
57 | func GetApnsMsgByTenantId(tenantId string) ([]*ApnsMsg, error) {
58 |
59 | rows, _ := db.MySQL.Query(SelectApnsMsgByTenantIdSQL, tenantId)
60 |
61 | ret := []*ApnsMsg{}
62 | for rows.Next() {
63 | apnsmsg := ApnsMsg{}
64 | if err := rows.Scan(&apnsmsg.Id, &apnsmsg.Type, &apnsmsg.ApnsToken, &apnsmsg.Cnt, &apnsmsg.TenantId, &apnsmsg.Pushed, &apnsmsg.Created, &apnsmsg.Updated); err != nil {
65 | logger.Error(err)
66 |
67 | return nil, err
68 | }
69 | ret = append(ret, &apnsmsg)
70 | }
71 |
72 | return ret, nil
73 | }
74 |
75 | // 在数据库中查询资源.
76 | func GetApnsMsgByApnsToken(apnsToken string) ([]*ApnsMsg, error) {
77 |
78 | rows, _ := db.MySQL.Query(SelectApnsMsgByApnsTokenSQL, apnsToken)
79 |
80 | ret := []*ApnsMsg{}
81 | for rows.Next() {
82 | apnsmsg := ApnsMsg{}
83 | if err := rows.Scan(&apnsmsg.Id, &apnsmsg.Type, &apnsmsg.ApnsToken, &apnsmsg.Cnt, &apnsmsg.TenantId, &apnsmsg.Pushed, &apnsmsg.Created, &apnsmsg.Updated); err != nil {
84 | logger.Error(err)
85 |
86 | return nil, err
87 | }
88 | ret = append(ret, &apnsmsg)
89 | }
90 |
91 | return ret, nil
92 | }
93 |
94 | // 数据库中插入资源
95 | func AddApnsMsg(apnsMsg *ApnsMsg) (*ApnsMsg, bool) {
96 | tx, err := db.MySQL.Begin()
97 |
98 | if err != nil {
99 | logger.Error(err)
100 | return nil, false
101 | }
102 |
103 | // 创建资源记录
104 | _, err = tx.Exec(InsertResourceSQL, apnsMsg.Id, apnsMsg.Type, apnsMsg.ApnsToken, apnsMsg.Cnt, apnsMsg.TenantId, apnsMsg.Pushed, apnsMsg.Created, apnsMsg.Updated)
105 | if err != nil {
106 | logger.Error(err)
107 |
108 | if err := tx.Rollback(); err != nil {
109 | logger.Error(err)
110 | }
111 | return nil, false
112 | }
113 |
114 | if err := tx.Commit(); err != nil {
115 | logger.Error(err)
116 | return nil, false
117 | }
118 | return apnsMsg, true
119 | }
120 |
121 | // 数据库中插入资源
122 | func UpdateApnsMsgById(apnsMsg *ApnsMsg) (*ApnsMsg, bool) {
123 | tx, err := db.MySQL.Begin()
124 |
125 | if err != nil {
126 | logger.Error(err)
127 | return nil, false
128 | }
129 |
130 | // 创建资源记录
131 | _, err = tx.Exec(UpdateApnsMsgByIdSQL, apnsMsg.Type, apnsMsg.ApnsToken, apnsMsg.Cnt, apnsMsg.TenantId, apnsMsg.Pushed, apnsMsg.Created, apnsMsg.Updated, apnsMsg.Id)
132 | if err != nil {
133 | logger.Error(err)
134 |
135 | if err := tx.Rollback(); err != nil {
136 | logger.Error(err)
137 | }
138 | return nil, false
139 | }
140 |
141 | if err := tx.Commit(); err != nil {
142 | logger.Error(err)
143 | return nil, false
144 | }
145 | return apnsMsg, true
146 | }
147 |
148 | func DeleteApnsMsgById(id string) bool {
149 | tx, err := db.MySQL.Begin()
150 |
151 | if err != nil {
152 | logger.Error(err)
153 |
154 | return false
155 | }
156 |
157 | _, err = tx.Exec(DelApnsMsgByIdSQL, id)
158 | if err != nil {
159 | logger.Error(err)
160 |
161 | if err := tx.Rollback(); err != nil {
162 | logger.Error(err)
163 | }
164 |
165 | return false
166 | }
167 |
168 | if err := tx.Commit(); err != nil {
169 | logger.Error(err)
170 |
171 | return false
172 | }
173 |
174 | return true
175 | }
176 |
177 | func DeleteApnsMsgByTenantId(tenantId string) bool {
178 | tx, err := db.MySQL.Begin()
179 |
180 | if err != nil {
181 | logger.Error(err)
182 |
183 | return false
184 | }
185 |
186 | _, err = tx.Exec(DelApnsMsgByTenantIdSQL, tenantId)
187 | if err != nil {
188 | logger.Error(err)
189 |
190 | if err := tx.Rollback(); err != nil {
191 | logger.Error(err)
192 | }
193 |
194 | return false
195 | }
196 |
197 | if err := tx.Commit(); err != nil {
198 | logger.Error(err)
199 |
200 | return false
201 | }
202 |
203 | return true
204 | }
205 |
--------------------------------------------------------------------------------
/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/b3log/wide/log"
5 | "os"
6 | )
7 |
8 | // 定义应用端操作结构.
9 | type app struct{}
10 |
11 | // 声明应用端操作接口.
12 | var App = app{}
13 |
14 | var logger = log.NewLogger(os.Stdout)
15 |
--------------------------------------------------------------------------------
/app/appWeb.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | // 定义网页端操作结构.
4 | type appWeb struct{}
5 |
6 | // 声明网页端操作接口.
7 | var AppWeb = appWeb{}
8 |
9 | const (
10 | APPWEB_TYPE = "appWeb"
11 | )
12 |
--------------------------------------------------------------------------------
/app/config.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package app
18 |
19 | import (
20 | "flag"
21 | "fmt"
22 | "github.com/Terry-Mao/goconf"
23 | "runtime"
24 | "time"
25 | )
26 |
27 | var (
28 | Conf *Config
29 | confFile string
30 | )
31 |
32 | const (
33 | /*后缀信息*/
34 | TENANT_SUFFIX = "@tenant"
35 | ORG_SUFFIX = "@org"
36 | QUN_SUFFIX = "@qun"
37 | USER_SUFFIX = "@user"
38 | APP_SUFFIX = "@app"
39 | )
40 |
41 | // InitConfig initialize config file path
42 | func init() {
43 | flag.StringVar(&confFile, "c", "./web.conf", " set web config file path")
44 | }
45 |
46 | type Config struct {
47 | HttpBind []string `goconf:"base:http.bind:,"`
48 | AdminBind []string `goconf:"base:admin.bind:,"`
49 | AppBind []string `goconf:"base:app.bind:,"`
50 | MaxProc int `goconf:"base:maxproc"`
51 | PprofBind []string `goconf:"base:pprof.bind:,"`
52 | User string `goconf:"base:user"`
53 | PidFile string `goconf:"base:pidfile"`
54 | Dir string `goconf:"base:dir"`
55 | Router string `goconf:"base:router"`
56 | QQWryPath string `goconf:"res:qqwry.path"`
57 | ZookeeperAddr []string `goconf:"zookeeper:addr:,"`
58 | ZookeeperTimeout time.Duration `goconf:"zookeeper:timeout:time"`
59 | ZookeeperCometPath string `goconf:"zookeeper:comet.path"`
60 | ZookeeperMessagePath string `goconf:"zookeeper:message.path"`
61 | RPCRetry time.Duration `goconf:"rpc:retry:time"`
62 | RPCPing time.Duration `goconf:"rpc:ping:time"`
63 | RedisSource map[string]string `goconf:"-"`
64 | RedisIdleTimeout time.Duration `goconf:"redis:timeout:time"`
65 | RedisMaxIdle int `goconf:"redis:idle"`
66 | RedisMaxActive int `goconf:"redis:active"`
67 | RedisMaxStore int `goconf:"redis:store"`
68 | RedisKetamaBase int `goconf:"redis:ketama.base"`
69 | TokenExpire int `goconf:"token:expire"`
70 | ApnsType string `goconf:"apns:type"`
71 | WeedfsAddr string `goconf:"weedfs:address:"`
72 | MsgExpire int `goconf:"msg:expire"`
73 | }
74 |
75 | // InitConfig init configuration file.
76 | func InitConfig() error {
77 | gconf := goconf.New()
78 | if err := gconf.Parse(confFile); err != nil {
79 | logger.Errorf("goconf.Parse(\"%s\") error(%v)", confFile, err)
80 | return err
81 | }
82 | // Default config
83 | Conf = &Config{
84 | HttpBind: []string{"localhost:80"},
85 | AdminBind: []string{"localhost:81"},
86 | MaxProc: runtime.NumCPU(),
87 | PprofBind: []string{"localhost:8190"},
88 | User: "nobody nobody",
89 | PidFile: "/tmp/gopush-cluster-web.pid",
90 | Dir: "./",
91 | Router: "",
92 | QQWryPath: "/tmp/QQWry.dat",
93 | ZookeeperAddr: []string{":2181"},
94 | ZookeeperTimeout: 30 * time.Second,
95 | ZookeeperCometPath: "/gopush-cluster-comet",
96 | ZookeeperMessagePath: "/gopush-cluster-message",
97 | RPCRetry: 3 * time.Second,
98 | RPCPing: 1 * time.Second,
99 | RedisSource: make(map[string]string),
100 | }
101 |
102 | if err := gconf.Unmarshal(Conf); err != nil {
103 | logger.Errorf("goconf.Unmarshall() error(%v)", err)
104 | return err
105 | }
106 |
107 | redisAddrsSec := gconf.Get("redis.source")
108 | if redisAddrsSec != nil {
109 | for _, key := range redisAddrsSec.Keys() {
110 | addr, err := redisAddrsSec.String(key)
111 | if err != nil {
112 | return fmt.Errorf("config section: \"redis.addrs\" key: \"%s\" error(%v)", key, err)
113 | }
114 | Conf.RedisSource[key] = addr
115 | }
116 | }
117 | return nil
118 | }
119 |
--------------------------------------------------------------------------------
/app/contact.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/EPICPaaS/appmsgsrv/db"
6 | "github.com/EPICPaaS/go-uuid/uuid"
7 | "io/ioutil"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | const (
13 | // 添加联系人.
14 | InsertUserUserSQL = "INSERT INTO `user_user` (`id`, `from_user_id`, `to_user_id`, `remark_name`, `sort`,`created`, `updated`) VALUES " +
15 | "(?, ?, ?, ?, ?, ?, ?)"
16 | // 删除联系人.
17 | DeleteUserUserSQL = "DELETE FROM `user_user` WHERE `from_user_id` = ? AND `to_user_id` = ?"
18 | )
19 |
20 | // 联系人结构.
21 | type UserUser struct {
22 | Id string `json:"id"`
23 | FromUserId string `json:"fromUserId"`
24 | ToUserId string `json:"toUserId"`
25 | RemarkName string `json:"remarkName"`
26 | Sort int `json:"sort"`
27 | Created time.Time `json:"created"`
28 | Updated time.Time `json:"updated"`
29 | }
30 |
31 | /*添加或删除联系人 , 当请求参数starFriend为1时添加联系人,否则删除*/
32 | func (*device) AddOrRemoveContact(w http.ResponseWriter, r *http.Request) {
33 | if r.Method != "POST" {
34 | http.Error(w, "Method Not Allowed", 405)
35 | return
36 | }
37 |
38 | baseRes := baseResponse{OK, ""}
39 | body := ""
40 | res := map[string]interface{}{"baseResponse": &baseRes}
41 | defer RetPWriteJSON(w, r, res, &body, time.Now())
42 |
43 | bodyBytes, err := ioutil.ReadAll(r.Body)
44 | if err != nil {
45 | baseRes.Ret = ParamErr
46 | logger.Errorf("ioutil.ReadAll() failed (%s)", err.Error())
47 |
48 | return
49 | }
50 | body = string(bodyBytes)
51 |
52 | var args map[string]interface{}
53 |
54 | if err := json.Unmarshal(bodyBytes, &args); err != nil {
55 | baseRes.ErrMsg = err.Error()
56 | baseRes.Ret = ParamErr
57 |
58 | return
59 | }
60 |
61 | baseReq := args["baseRequest"].(map[string]interface{})
62 |
63 | // Token 校验
64 | token := baseReq["token"].(string)
65 | user := getUserByToken(token)
66 | if nil == user {
67 | baseRes.Ret = AuthErr
68 |
69 | return
70 | }
71 |
72 | fromUserId := baseReq["uid"].(string)
73 | toUserId := args["uid"].(string)
74 | starFriend := int(args["starFriend"].(float64))
75 |
76 | now := time.Now()
77 |
78 | if 1 == starFriend { // 添加联系人
79 | if isStar(fromUserId, toUserId) {
80 | return
81 | }
82 |
83 | userUser := UserUser{Id: uuid.New(), FromUserId: fromUserId, ToUserId: toUserId, RemarkName: "", Sort: 0,
84 | Created: now, Updated: now}
85 |
86 | if !createContact(&userUser) {
87 | baseRes.Ret = InternalErr
88 |
89 | return
90 | }
91 |
92 | logger.Tracef("Created a contact [from=%s, to=%s]", fromUserId, toUserId)
93 | } else { // 删除联系人
94 | if !deleteContact(fromUserId, toUserId) {
95 | baseRes.Ret = InternalErr
96 |
97 | return
98 | }
99 |
100 | logger.Tracef("Deleted a contact [from=%s, to=%s]", fromUserId, toUserId)
101 | }
102 | }
103 |
104 | // 数据库中插入联系人记录.
105 | func createContact(userUser *UserUser) bool {
106 | tx, err := db.MySQL.Begin()
107 |
108 | if err != nil {
109 | logger.Error(err)
110 |
111 | return false
112 | }
113 |
114 | _, err = tx.Exec(InsertUserUserSQL, userUser.Id, userUser.FromUserId, userUser.ToUserId, userUser.RemarkName,
115 | userUser.Sort, userUser.Created, userUser.Updated)
116 | if err != nil {
117 | logger.Error(err)
118 |
119 | if err := tx.Rollback(); err != nil {
120 | logger.Error(err)
121 | }
122 |
123 | return false
124 | }
125 |
126 | if err := tx.Commit(); err != nil {
127 | logger.Error(err)
128 |
129 | return false
130 | }
131 |
132 | return true
133 | }
134 |
135 | // 在数据库中删除联系人记录.
136 | func deleteContact(fromUserId, toUserId string) bool {
137 | tx, err := db.MySQL.Begin()
138 |
139 | if err != nil {
140 | logger.Error(err)
141 |
142 | return false
143 | }
144 |
145 | _, err = tx.Exec(DeleteUserUserSQL, fromUserId, toUserId)
146 | if err != nil {
147 | logger.Error(err)
148 |
149 | if err := tx.Rollback(); err != nil {
150 | logger.Error(err)
151 | }
152 |
153 | return false
154 | }
155 |
156 | if err := tx.Commit(); err != nil {
157 | logger.Error(err)
158 |
159 | return false
160 | }
161 |
162 | return true
163 | }
164 |
--------------------------------------------------------------------------------
/app/device.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | // 定义移动端操作结构.
4 | type device struct{}
5 |
6 | // 声明移动端操作接口.
7 | var Device = device{}
8 |
9 | const (
10 | DEVICE_TYPE_IOS = "ios"
11 | DEVICE_TYPE_ANDROID = "android"
12 | )
13 |
--------------------------------------------------------------------------------
/app/fileLink.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/EPICPaaS/appmsgsrv/db"
6 | "github.com/EPICPaaS/go-uuid/uuid"
7 | "io/ioutil"
8 | "net/http"
9 | "strconv"
10 | "strings"
11 | "time"
12 | )
13 |
14 | const (
15 | INSERT_FILELINK = "insert into file_link (id , sender_id,file_id ,file_name,file_url,size,created,updated) values(?,?,?,?,?,?,?,?)"
16 | UPDATE_FILELINK_TIME = "update file_link set updated =? where sender_id =? and file_id =?"
17 | EXIST_FILELINK = "select id from file_link where sender_id =? and file_id =?"
18 | SELECT_EXPIRE_FILELINK = "select id, file_url from file_link where updated < ?"
19 | )
20 |
21 | var ScanFileTime = time.NewTicker(24 * time.Hour)
22 |
23 | type FileLink struct {
24 | Id string
25 | SenderId string
26 | FileId string
27 | FileName string
28 | FileUrl string
29 | Size int
30 | Created time.Time
31 | Updated time.Time
32 | }
33 |
34 | /*保存文件链接信息*/
35 | func SaveFileLinK(fileLink *FileLink) bool {
36 | tx, err := db.MySQL.Begin()
37 | if err != nil {
38 | logger.Error(err)
39 | return false
40 | }
41 | //更新
42 | if ExistFileLink(fileLink) {
43 | _, err = tx.Exec(UPDATE_FILELINK_TIME, time.Now().Local(), fileLink.SenderId, fileLink.FileId)
44 | } else { //新增
45 | _, err = tx.Exec(INSERT_FILELINK, uuid.New(), fileLink.SenderId, fileLink.FileId, fileLink.FileName, fileLink.FileUrl, fileLink.Size, time.Now().Local(), time.Now().Local())
46 | }
47 | if err != nil {
48 | logger.Error(err)
49 | if err := tx.Rollback(); err != nil {
50 | logger.Error(err)
51 | }
52 | return false
53 | }
54 |
55 | if err := tx.Commit(); err != nil {
56 | return false
57 | }
58 |
59 | return true
60 | }
61 |
62 | /*判断是否存在文件链接记录*/
63 | func ExistFileLink(fileLink *FileLink) bool {
64 | rows, err := db.MySQL.Query(EXIST_FILELINK, fileLink.SenderId, fileLink.FileId)
65 | if err != nil {
66 | logger.Error(err)
67 | return false
68 | }
69 |
70 | defer rows.Close()
71 |
72 | if err = rows.Err(); err != nil {
73 | logger.Error(err)
74 | return false
75 | }
76 | return rows.Next()
77 | }
78 |
79 | /*删除weedfs服务器文件*/
80 | func DeleteFile(fileUrl string) bool {
81 |
82 | var url = "http://" + fileUrl
83 | client := &http.Client{}
84 | req, err := http.NewRequest("DELETE", url, nil)
85 | if err != nil {
86 | logger.Errorf("delete file fail [ERROR]-%s", err.Error())
87 | return false
88 | }
89 | resp, err := client.Do(req)
90 | if err != nil {
91 | logger.Errorf("delete file fail [ERROR]-%s", err.Error())
92 | return false
93 | }
94 |
95 | body, err := ioutil.ReadAll(resp.Body)
96 | if err != nil {
97 | logger.Errorf("ioutil.ReadAll() failed (%s)", err.Error())
98 | return false
99 | }
100 | var respBody map[string]interface{}
101 | if err := json.Unmarshal(body, &respBody); err != nil {
102 | logger.Errorf("convert to json failed (%s)", err.Error())
103 | return false
104 | }
105 | e, ok := respBody["error"].(string)
106 | if ok {
107 | logger.Errorf("delete file fail [ERROR]- %s", e)
108 | return false
109 | }
110 | return true
111 | }
112 |
113 | /*定时扫描过期的文件链接,如果过期侧删除该文件记录和文件服务器中的文件*/
114 | func ScanExpireFileLink() {
115 |
116 | //构造时间差
117 | subTimeStr := strconv.Itoa(Conf.MsgExpire)
118 | subTime, _ := time.ParseDuration("-" + subTimeStr + "s")
119 | /*定时任务删除,过期聊天文件*/
120 | for _ = range ScanFileTime.C {
121 |
122 | n := time.Now().Local()
123 |
124 | expire := time.Now().Local().Add(subTime)
125 | rows, err := db.MySQL.Query(SELECT_EXPIRE_FILELINK, expire)
126 | if err != nil {
127 | logger.Error(err)
128 | continue
129 | }
130 |
131 | if err := rows.Err(); err != nil {
132 | rows.Close()
133 | logger.Error(err)
134 | continue
135 | }
136 |
137 | var delIds []string
138 | for rows.Next() {
139 | var id, fileUrl string
140 | if err := rows.Scan(&id, &fileUrl); err != nil {
141 | logger.Error(err)
142 | rows.Close()
143 | continue
144 | }
145 |
146 | //删除文件
147 | if DeleteFile(fileUrl) {
148 | delIds = append(delIds, id)
149 | }
150 | }
151 |
152 | //查询完毕后关闭链接
153 | rows.Close()
154 |
155 | /*删除文件记录*/
156 | if len(delIds) > 0 {
157 | tx, err := db.MySQL.Begin()
158 | if err != nil {
159 | logger.Error(err)
160 | continue
161 | }
162 | delSql := "delete from file_link where id in ('" + strings.Join(delIds, "','") + "')"
163 | _, err = tx.Exec(delSql)
164 | if err != nil {
165 | logger.Error(err)
166 | if err := tx.Rollback(); err != nil {
167 | logger.Error(err)
168 | }
169 | continue
170 | }
171 | //提交操作
172 | if err := tx.Commit(); err != nil {
173 | logger.Error(err)
174 | continue
175 | }
176 | }
177 | //为了查看扫描性能
178 | logger.Infof("scan file succeed --- %v \n", time.Since(n))
179 | }
180 | logger.Error("scan expire file logout ")
181 | }
182 |
--------------------------------------------------------------------------------
/app/resource.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/EPICPaaS/appmsgsrv/db"
7 |
8 | //"github.com/EPICPaaS/go-uuid/uuid"
9 | )
10 |
11 | const (
12 |
13 | // 租户资源插入 SQL.
14 | InsertResourceSQL = "INSERT INTO `resource` (`id`, `customer_id`, `name`, `description`, `type`, `content`, `created`, `updated`) VALUES " +
15 | "(?, ?, ?, ?, ?, ?, ?, ?)"
16 |
17 | // 根据资源id 获取资源
18 | SelectResourceByIdSQL = "SELECT * FROM `resource` where `id` = ?"
19 |
20 | // 根据租户 id 查询租户的资源.
21 | SelectResourceByTenantIdSQL = "SELECT * FROM `resource` where `customer_id` = (select customer_id from tenant where id = ?)"
22 |
23 | //根据id修改资源
24 | UpdateResourceByIdSQL = "UPDATE `resource` SET `customer_id` = ? ,`name` = ? , `description` = ? , `type` = ? ,`content` = ? , `created` = ? , `updated` = ? WHERE `id` = ?"
25 |
26 | //根据id删除资源
27 | DelResourceByIdSQL = "DELETE FROM `resource` WHERE `id` =? "
28 | )
29 |
30 | //租户证书资源结构
31 | type Resource struct {
32 | Id string `json:"id"`
33 | CustomerId string `json:"customerId"`
34 | Name string `json:"name"`
35 | Description string `json:"description"`
36 | Type string `json:"type"`
37 | Content string `json:"content"`
38 | Created time.Time `json:"created"`
39 | Updated time.Time `json:"updated"`
40 | }
41 |
42 | // 在数据库中查询资源.
43 | func GetResourceById(resourceId string) (*Resource, error) {
44 | row := db.MySQL.QueryRow(SelectResourceByIdSQL, resourceId)
45 |
46 | resource := Resource{}
47 | if err := row.Scan(&resource.Id, &resource.CustomerId, &resource.Name, &resource.Description, &resource.Type, &resource.Content, &resource.Created, &resource.Updated); err != nil {
48 | logger.Error(err)
49 |
50 | return nil, err
51 | }
52 |
53 | return &resource, nil
54 | }
55 |
56 | // 在数据库中查询资源.
57 | func GetResourceByTenantId(tenantId string) ([]*Resource, error) {
58 |
59 | rows, _ := db.MySQL.Query(SelectResourceByTenantIdSQL, tenantId)
60 | if rows != nil {
61 | defer rows.Close()
62 | }
63 | ret := []*Resource{}
64 | for rows.Next() {
65 | resource := &Resource{}
66 | if err := rows.Scan(&resource.Id, &resource.CustomerId, &resource.Name, &resource.Description, &resource.Type, &resource.Content, &resource.Created, &resource.Updated); err != nil {
67 | logger.Error(err)
68 |
69 | return nil, err
70 | }
71 | ret = append(ret, resource)
72 | }
73 |
74 | return ret, nil
75 | }
76 |
77 | // 数据库中插入资源
78 | func AddResource(resource *Resource) (*Resource, bool) {
79 | tx, err := db.MySQL.Begin()
80 |
81 | if err != nil {
82 | logger.Error(err)
83 | return nil, false
84 | }
85 |
86 | // 创建资源记录
87 | _, err = tx.Exec(InsertResourceSQL, resource.Id, resource.CustomerId, resource.Name, resource.Description, resource.Type, resource.Content, resource.Created, resource.Updated)
88 | if err != nil {
89 | logger.Error(err)
90 |
91 | if err := tx.Rollback(); err != nil {
92 | logger.Error(err)
93 | }
94 | return nil, false
95 | }
96 |
97 | if err := tx.Commit(); err != nil {
98 | logger.Error(err)
99 | return nil, false
100 | }
101 | return resource, true
102 | }
103 |
104 | // 数据库中插入资源
105 | func UpdateResource(resource *Resource) (*Resource, bool) {
106 | tx, err := db.MySQL.Begin()
107 |
108 | if err != nil {
109 | logger.Error(err)
110 | return nil, false
111 | }
112 |
113 | // 创建资源记录
114 | _, err = tx.Exec(UpdateResourceByIdSQL, resource.CustomerId, resource.Name, resource.Description, resource.Type, resource.Content, resource.Created, resource.Updated, resource.Id)
115 | if err != nil {
116 | logger.Error(err)
117 |
118 | if err := tx.Rollback(); err != nil {
119 | logger.Error(err)
120 | }
121 | return nil, false
122 | }
123 |
124 | if err := tx.Commit(); err != nil {
125 | logger.Error(err)
126 | return nil, false
127 | }
128 | return resource, true
129 | }
130 |
131 | //删除资源
132 | func DeleteResourceById(resourceId string) bool {
133 | tx, err := db.MySQL.Begin()
134 |
135 | if err != nil {
136 | logger.Error(err)
137 |
138 | return false
139 | }
140 |
141 | _, err = tx.Exec(DelResourceByIdSQL, resourceId)
142 | if err != nil {
143 | logger.Error(err)
144 |
145 | if err := tx.Rollback(); err != nil {
146 | logger.Error(err)
147 | }
148 |
149 | return false
150 | }
151 |
152 | if err := tx.Commit(); err != nil {
153 | logger.Error(err)
154 |
155 | return false
156 | }
157 |
158 | return true
159 | }
160 |
--------------------------------------------------------------------------------
/app/ret.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "time"
8 | )
9 |
10 | const (
11 | OK = 0
12 | NotFoundServer = 1001
13 | NotFound = 65531
14 | TooLong = 65532
15 | AuthErr = 65533
16 | ParamErr = 65534
17 | InternalErr = 65535
18 | OverQuotaApicall = 65536
19 | OverQuotaPush = 65537
20 | DeleteUser = 65538
21 | LoginErr = 65539
22 | )
23 |
24 | // 响应基础结构.
25 | type baseResponse struct {
26 | Ret int `json:"ret"`
27 | ErrMsg string `json:"errMsg"`
28 | }
29 |
30 | // retWrite marshal the result and write to client(get).
31 | func RetWrite(w http.ResponseWriter, r *http.Request, res map[string]interface{}, callback string, start time.Time) {
32 | data, err := json.Marshal(res)
33 | if err != nil {
34 | logger.Errorf("json.Marshal(\"%v\") error(%v)", res, err)
35 | return
36 | }
37 | dataStr := ""
38 | if callback == "" {
39 | // Normal json
40 | dataStr = string(data)
41 | } else {
42 | // Jsonp
43 | dataStr = fmt.Sprintf("%s(%s)", callback, string(data))
44 | }
45 | if n, err := w.Write([]byte(dataStr)); err != nil {
46 | logger.Errorf("w.Write(\"%s\") error(%v)", dataStr, err)
47 | } else {
48 | logger.Tracef("w.Write(\"%s\") write %d bytes", dataStr, n)
49 | }
50 |
51 | logger.Tracef("req: \"%s\", res:\"%s\", ip:\"%s\", time:\"%fs\"", r.URL.String(), dataStr, r.RemoteAddr, time.Now().Sub(start).Seconds())
52 | }
53 |
54 | // retPWrite marshal the result and write to client(post).
55 | func RetPWrite(w http.ResponseWriter, r *http.Request, res map[string]interface{}, body *string, start time.Time) {
56 | data, err := json.Marshal(res)
57 | if err != nil {
58 | logger.Errorf("json.Marshal(\"%v\") error(%v)", res, err)
59 | return
60 | }
61 | dataStr := string(data)
62 | if n, err := w.Write([]byte(dataStr)); err != nil {
63 | logger.Errorf("w.Write(\"%s\") error(%v)", dataStr, err)
64 | } else {
65 | logger.Tracef("w.Write(\"%s\") write %d bytes", dataStr, n)
66 | }
67 |
68 | logger.Tracef("req: \"%s\", post: \"%s\", res:\"%s\", ip:\"%s\", time:\"%fs\"", r.URL.String(), *body, dataStr, r.RemoteAddr, time.Now().Sub(start).Seconds())
69 | }
70 |
71 | // 带 Content-Type=application/json 头 写 JSON 数据,为了保持原有 gopush-cluster 的兼容性,所以新加了这个函数.
72 | func RetPWriteJSON(w http.ResponseWriter, r *http.Request, res map[string]interface{}, body *string, start time.Time) {
73 | w.Header().Set("Content-Type", "application/json")
74 |
75 | RetPWrite(w, r, res, body, start)
76 | }
77 |
--------------------------------------------------------------------------------
/app/session.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/EPICPaaS/appmsgsrv/session"
6 | "io/ioutil"
7 | "net/http"
8 | "strings"
9 | "time"
10 | )
11 |
12 | /*请求参数
13 | baseRequest: {
14 | "uid": "",
15 | "deviceID": "",
16 | "deviceType": "", // iOS / Android
17 | "token": ""
18 | }
19 | state:"active"
20 | sessionId:"1111"
21 | 设置会话状态(active/inactive)
22 | */
23 | func SessionStat(w http.ResponseWriter, r *http.Request) {
24 |
25 | if r.Method != "POST" {
26 | http.Error(w, "Method Not Allowed", 405)
27 | return
28 | }
29 |
30 | baseRes := baseResponse{OK, ""}
31 | body := ""
32 | res := map[string]interface{}{"baseResponse": &baseRes}
33 | defer RetPWriteJSON(w, r, res, &body, time.Now())
34 |
35 | bodyBytes, err := ioutil.ReadAll(r.Body)
36 | if err != nil {
37 | res["ret"] = ParamErr
38 | logger.Errorf("ioutil.ReadAll() failed (%s)", err.Error())
39 | return
40 | }
41 | body = string(bodyBytes)
42 |
43 | var args map[string]interface{}
44 |
45 | if err := json.Unmarshal(bodyBytes, &args); err != nil {
46 | baseRes.ErrMsg = err.Error()
47 | baseRes.Ret = ParamErr
48 | return
49 | }
50 |
51 | baseReq := args["baseRequest"].(map[string]interface{})
52 | deviceType, ok := baseReq["deviceType"].(string)
53 | if ok {
54 | deviceType = strings.ToLower(deviceType)
55 | }
56 | /* Token 校验,分为用户校验和应用校验*/
57 | token := baseReq["token"].(string)
58 | //用户校验
59 | if deviceType == DEVICE_TYPE_IOS || deviceType == DEVICE_TYPE_ANDROID {
60 | user := getUserByToken(token)
61 | if nil == user {
62 | baseRes.Ret = AuthErr
63 | return
64 | }
65 | } else { //应用校验
66 | _, err := getApplicationByToken(token)
67 | if nil != err {
68 | baseRes.Ret = AuthErr
69 | return
70 | }
71 | }
72 | //修改会话状态
73 | state := args["state"].(string)
74 | sessionId := args["sessionId"].(string)
75 | if ("active" == state || "inactive" == state) && len(sessionId) > 0 {
76 | if !session.SetSessionStat(sessionId, state) {
77 | baseRes.Ret = NotFound
78 | baseRes.ErrMsg = "设置会话状态失败!"
79 | return
80 | }
81 | } else {
82 | baseRes.Ret = ParamErr
83 | baseRes.ErrMsg = "参数格式错误,state值只能是:active/inactive,会话id不能为空)"
84 | return
85 | }
86 | }
87 |
88 | /*
89 | baseRequest: {
90 | "uid": "",
91 | "deviceID": "",
92 | "deviceType": "", // iOS / Android
93 | "token": ""
94 | }
95 | 获取用户会话session
96 | */
97 | func (*app) GetSession(w http.ResponseWriter, r *http.Request) {
98 | if r.Method != "POST" {
99 | http.Error(w, "Method Not Allowed", 405)
100 | return
101 | }
102 |
103 | baseRes := baseResponse{OK, ""}
104 | body := ""
105 | res := map[string]interface{}{"baseResponse": &baseRes}
106 | defer RetPWriteJSON(w, r, res, &body, time.Now())
107 |
108 | bodyBytes, err := ioutil.ReadAll(r.Body)
109 | if err != nil {
110 | res["ret"] = ParamErr
111 | logger.Errorf("ioutil.ReadAll() failed (%s)", err.Error())
112 | return
113 | }
114 | body = string(bodyBytes)
115 |
116 | var args map[string]interface{}
117 |
118 | if err := json.Unmarshal(bodyBytes, &args); err != nil {
119 | baseRes.ErrMsg = err.Error()
120 | baseRes.Ret = ParamErr
121 | return
122 | }
123 |
124 | baseReq := args["baseRequest"].(map[string]interface{})
125 | token := baseReq["token"].(string)
126 |
127 | //应用校验
128 | _, err = getApplicationByToken(token)
129 | if nil != err {
130 | baseRes.Ret = AuthErr
131 | return
132 | }
133 |
134 | //获取用户会话列表
135 | userId := baseReq["uid"].(string)
136 | ret := session.GetSessions(userId, []string{"all"})
137 | res["memberList"] = ret
138 |
139 | return
140 | }
141 |
--------------------------------------------------------------------------------
/appweb/conf/app.conf:
--------------------------------------------------------------------------------
1 | appname = appweb
2 | httpport = 8080
3 | runmode = dev
4 |
5 | driverName= mysql
6 | dataSource= root:123456@tcp(10.180.120.63:3308)/appmsgsrv?charset=utf8&loc=UTC
7 | maxIdle= 10
8 | maxOpen= 50
--------------------------------------------------------------------------------
/appweb/controllers/index.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "github.com/EPICPaaS/appmsgsrv/appweb/models"
5 | "github.com/astaxie/beego"
6 | )
7 |
8 | type IndexController struct {
9 | beego.Controller
10 | }
11 |
12 | func (this *IndexController) Get() {
13 | this.TplNames = "index.html"
14 | user := this.GetSession("user").(*models.User)
15 | orgs := models.GetRootOrg(user.TenantId)
16 | this.Data["orgs"] = orgs
17 | this.Data["User"] = user
18 | users := models.GetOrgUsersById("23622391649369951")
19 | this.Data["users"] = users
20 | }
21 |
22 | //获取下一单位
23 | func (this *IndexController) GetChildOrgs() {
24 |
25 | parentId := this.Input().Get("parentId")
26 | orgs := models.GetChildOrgs(parentId)
27 | this.Data["json"] = orgs
28 | this.ServeJson()
29 | this.StopRun()
30 | }
31 |
32 | //获取单位用户
33 | func (this *IndexController) GetOrgUserById() {
34 | orgId := this.Input().Get("orgId")
35 | users := models.GetOrgUsersById(orgId)
36 | this.Data["json"] = users
37 | this.ServeJson()
38 | this.StopRun()
39 | }
40 |
41 | //获取用户的群
42 | func (this *IndexController) GetMyQun() {
43 |
44 | user := this.GetSession("user").(*models.User)
45 | this.Data["json"] = models.GetMyQun(user.Id)
46 | this.ServeJson()
47 | this.StopRun()
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/appweb/controllers/ret.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | type Result struct {
4 | Success bool
5 | Msg string
6 | Data interface{}
7 | }
8 |
--------------------------------------------------------------------------------
/appweb/controllers/user.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | //"fmt"
5 | "github.com/EPICPaaS/appmsgsrv/appweb/models"
6 | "github.com/astaxie/beego"
7 | )
8 |
9 | /*user控制器*/
10 | type UserController struct {
11 | beego.Controller
12 | }
13 |
14 | func (this *UserController) Get() {
15 |
16 | this.TplNames = "login.html"
17 | this.Data["Tenants"] = models.GetAll()
18 | }
19 |
20 | func (this *UserController) Post() {
21 |
22 | //返回结果
23 | ret := Result{
24 | Success: true,
25 | }
26 | uname := this.Input().Get("userName")
27 | tenantId := this.Input().Get("tenantId")
28 | pwd := this.Input().Get("password")
29 | user := models.GetUserByNameTenantId(uname, tenantId)
30 |
31 | if user != nil && user.Password == pwd {
32 | ret.Data = user
33 | this.SetSession("user", user)
34 |
35 | } else {
36 | ret.Success = false
37 | ret.Msg = "用户名或密码错误"
38 | }
39 |
40 | this.Data["json"] = ret
41 | this.ServeJson()
42 | this.StopRun()
43 | }
44 |
45 | func (this *UserController) Logout() {
46 | //清除缓存
47 | this.DelSession("user")
48 | this.Redirect("/", 302)
49 | }
50 |
--------------------------------------------------------------------------------
/appweb/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | _ "github.com/EPICPaaS/appmsgsrv/appweb/routers"
5 | _ "github.com/EPICPaaS/appmsgsrv/appweb/setting"
6 | "github.com/astaxie/beego"
7 | "runtime"
8 | )
9 |
10 | func main() {
11 |
12 | runtime.GOMAXPROCS(runtime.NumCPU())
13 | beego.Run()
14 | beego.SetStaticPath("/static", "static")
15 | }
16 |
--------------------------------------------------------------------------------
/appweb/models/org.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "bytes"
5 | "github.com/astaxie/beego"
6 | "github.com/astaxie/beego/orm"
7 | _ "github.com/go-sql-driver/mysql"
8 | "time"
9 | )
10 |
11 | type Tenant struct {
12 | Id string `orm:"column(id);pk"`
13 | Code string
14 | Name string
15 | Status int
16 | CustomerId string
17 | Created time.Time
18 | Updated time.Time
19 | }
20 |
21 | type Org struct {
22 | Id string `orm:"column(id);pk"`
23 | Name string
24 | ShortName string
25 | ParentId string
26 | Location string
27 | TenantId string
28 | Sort int
29 | }
30 |
31 | func GetAll() *[]Tenant {
32 |
33 | o := orm.NewOrm()
34 | tenants := make([]Tenant, 0)
35 | qs := o.QueryTable("tenant")
36 | _, err := qs.All(&tenants)
37 | if err != nil {
38 | beego.Error(err)
39 | return nil
40 | }
41 | return &tenants
42 | }
43 |
44 | //获取一级单位
45 | func GetRootOrg(tenantId string) *[]Org {
46 |
47 | if len(tenantId) == 0 {
48 | return nil
49 | }
50 | o := orm.NewOrm()
51 | qs := o.QueryTable("org")
52 | orgs := &[]Org{}
53 | _, err := qs.Filter("tenant_id", tenantId).Filter("parent_id", "").All(orgs)
54 | if err != nil {
55 | beego.Error(err)
56 | return nil
57 | }
58 | return orgs
59 | }
60 |
61 | //获取指定单位的直属下级单位
62 | func GetChildOrgs(parentId string) *[]Org {
63 | if len(parentId) == 0 {
64 | return nil
65 | }
66 | o := orm.NewOrm()
67 | qs := o.QueryTable("org")
68 | orgs := &[]Org{}
69 | _, err := qs.Filter("parent_id", parentId).All(orgs)
70 | if err != nil {
71 | beego.Error(err)
72 | return nil
73 | }
74 | return orgs
75 | }
76 |
77 | //获取指定单位的所有下级单位
78 | func GetOrgsByParentId(parentId string) *[]Org {
79 | if len(parentId) == 0 {
80 | return nil
81 | }
82 | o := orm.NewOrm()
83 | orgs := &[]Org{}
84 | _, err := o.Raw("select * from (select location from org where id =?) t1 , org t2 where t2.location like CONCAT(t1.location,'%')", parentId).QueryRows(orgs)
85 | if err != nil {
86 | beego.Error(err)
87 | return nil
88 | }
89 | return orgs
90 | }
91 |
92 | //获取单位下的所有人员
93 | func GetOrgUsersById(orgId string) *[]User {
94 | if len(orgId) == 0 {
95 | return nil
96 | }
97 | o := orm.NewOrm()
98 | //查询该单位的所有下级单位
99 | orgs := GetOrgsByParentId(orgId)
100 | if orgs != nil {
101 | users := &[]User{}
102 | //拼接查询语句
103 | selectUser := bytes.Buffer{}
104 | selectUser.WriteString("select * from user where id in (select user_id from org_user where ")
105 | ls := len(*orgs) - 1
106 | for i, org := range *orgs {
107 | selectUser.WriteString(" org_id =" + org.Id)
108 | if i != ls {
109 | selectUser.WriteString(" or ")
110 | } else {
111 | selectUser.WriteString(" )")
112 | }
113 | }
114 |
115 | _, err := o.Raw(selectUser.String()).QueryRows(users)
116 |
117 | if err != nil {
118 | beego.Error(err)
119 | }
120 | return users
121 | }
122 |
123 | return nil
124 | }
125 |
--------------------------------------------------------------------------------
/appweb/models/qun.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "github.com/astaxie/beego"
6 | "github.com/astaxie/beego/orm"
7 | _ "github.com/go-sql-driver/mysql"
8 | "time"
9 | )
10 |
11 | type Qun struct {
12 | Id string `orm:"column(id);pk"`
13 | CreatorId string
14 | Name string
15 | Description string
16 | MaxMember string
17 | Avatar string
18 | Created time.Time
19 | Updated time.Time
20 | }
21 |
22 | type QunUser struct {
23 | Id string `orm:"column(id);pk"`
24 | QunId string
25 | UserId string
26 | Sort int
27 | role int
28 | Created time.Time
29 | Updated time.Time
30 | }
31 |
32 | //获取用户所参与的群
33 | func GetMyQun(userId string) *[]Qun {
34 | if len(userId) == 0 {
35 | return nil
36 | }
37 |
38 | o := orm.NewOrm()
39 | quns := &[]Qun{}
40 | _, err := o.Raw("select * from qun where id in (SELECT qun_id from qun_user where user_id = ?)", userId).QueryRows(quns)
41 | if err != nil {
42 | beego.Error(err)
43 | return nil
44 | }
45 | fmt.Println(userId, "群:", quns)
46 | return quns
47 | }
48 |
--------------------------------------------------------------------------------
/appweb/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "github.com/astaxie/beego"
6 | "github.com/astaxie/beego/context"
7 | "github.com/astaxie/beego/orm"
8 | _ "github.com/go-sql-driver/mysql"
9 | "time"
10 | )
11 |
12 | type User struct {
13 | Id string `orm:"column(id);pk"`
14 | Name string
15 | Nickname string
16 | Avatar string
17 | NamePy string
18 | NameQuanpin string
19 | Status int
20 | Rand int
21 | Password string
22 | TenantId string
23 | Level int
24 | Email string
25 | Mobile string
26 | Area string
27 | Created time.Time
28 | Updated time.Time
29 | }
30 |
31 | /*获取*/
32 | func GetUserByNameTenantId(userName, tenantId string) *User {
33 |
34 | var users []User
35 | o := orm.NewOrm()
36 | num, err := o.Raw("SELECT * FROM user WHERE name = ? and tenant_id=?", userName, tenantId).QueryRows(&users)
37 | if err != nil {
38 | fmt.Println("查询出错")
39 | beego.Error(err)
40 | return nil
41 | }
42 | if num > 0 {
43 | return &users[0]
44 | }
45 | return nil
46 | }
47 |
48 | //验证用户是否登陆
49 | var IsUserLogin = func(ctx *context.Context) {
50 | user := ctx.Input.Session("user")
51 | if user == nil && ctx.Request.RequestURI != "/login" {
52 | ctx.Redirect(302, "/")
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/appweb/routers/router.go:
--------------------------------------------------------------------------------
1 | package routers
2 |
3 | import (
4 | "github.com/EPICPaaS/appmsgsrv/appweb/controllers"
5 | "github.com/astaxie/beego"
6 | )
7 |
8 | func init() {
9 | beego.Router("/", &controllers.UserController{})
10 | beego.Router("/login", &controllers.UserController{})
11 | beego.Router("/logout", &controllers.UserController{}, "get:Logout")
12 | beego.Router("/index", &controllers.IndexController{})
13 | beego.Router("/GetOrgUser", &controllers.IndexController{}, "post:GetOrgUserById")
14 | beego.Router("/GetMyQun", &controllers.IndexController{}, "post:GetMyQun")
15 | beego.Router("/GetChildOrgs", &controllers.IndexController{}, "post:GetChildOrgs")
16 | }
17 |
--------------------------------------------------------------------------------
/appweb/setting/setting.go:
--------------------------------------------------------------------------------
1 | package setting
2 |
3 | import (
4 | "github.com/EPICPaaS/appmsgsrv/appweb/models"
5 | "github.com/astaxie/beego"
6 | "github.com/astaxie/beego/orm"
7 | _ "github.com/go-sql-driver/mysql"
8 | )
9 |
10 | func init() {
11 | // 注册模型
12 | orm.Debug = true
13 | orm.RegisterModel(new(models.User), new(models.Tenant), new(models.Org))
14 | RegisterDB()
15 | InitSession()
16 | beego.InsertFilter("/index", beego.BeforeExec, models.IsUserLogin)
17 |
18 | }
19 |
20 | func RegisterDB() {
21 |
22 | driverName := beego.AppConfig.String("driverName")
23 | dataSource := beego.AppConfig.String("dataSource")
24 | maxIdle, _ := beego.AppConfig.Int("maxIdle")
25 | maxOpen, _ := beego.AppConfig.Int("maxOpen")
26 | // 注册驱动
27 | orm.RegisterDriver("mysql", orm.DR_MySQL)
28 | // 注册默认数据库
29 | // set default database
30 | err := orm.RegisterDataBase("default", driverName, dataSource, maxIdle, maxOpen)
31 | orm.RunCommand()
32 | //不自动建表
33 | err = orm.RunSyncdb("default", false, false)
34 | if err != nil {
35 | beego.Error(err)
36 | }
37 | }
38 |
39 | func InitSession() {
40 | beego.SessionOn = true
41 | beego.SessionProvider = "memory"
42 | beego.SessionGCMaxLifetime = 600 //60 seconds
43 | beego.SessionName = "yxUser"
44 | beego.SessionCookieLifeTime = 600 //60 seconds
45 | beego.SessionAutoSetCookie = true
46 | beego.SessionSavePath = "/"
47 | }
48 |
--------------------------------------------------------------------------------
/appweb/static/css/login.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 | /* CSS Document */
3 |
4 | /**
5 | * Copyright 远信科技, All Rights Reserved.
6 | */
7 |
8 | /**
9 | * @fileoverview 这是产品首页的css文件
10 | *
11 | * @author: Zhigang.li
12 | * @author Liyuan Li
13 | * @version 1.0.0.3, Feb 24, 2014
14 | */
15 | body {
16 | background: #369dcc;
17 | margin: 0;
18 | font-family: '微软雅黑';
19 | color: #FFF;
20 | background:url(/static/images/loginb.png) no-repeat;
21 |
22 | }
23 | div{
24 | box-sizing: border-box;
25 | }
26 |
27 | .wrapper {
28 | margin: 0 auto;
29 | width: 620px;
30 | overflow: hidden;
31 | }
32 | .logo{
33 | background: url(/static/images/logo.png) center center no-repeat;
34 | height:150px;
35 | width:600px;
36 | }
37 | .org{
38 | height:24px;
39 | }
40 | .selectOrg{
41 | float:left;
42 | margin-left:35px;
43 | width:110px;
44 | height:24px;
45 | text-align:center;
46 | font-size:14px;
47 | cursor:pointer;
48 | line-height:23px;
49 | background:url(/static/images/selectOrg.png) center bottom no-repeat;
50 | }
51 | .orgName{
52 | float:left;
53 | height:24px;
54 | }
55 | .loginForm {
56 | height:51px;
57 | width:620px;
58 | background: url(/static/images/loginForm.png) no-repeat center center;
59 | overflow:hidden;
60 | }
61 | .parameter{
62 | width:569px;
63 | height:51px;
64 | float:left;
65 | padding-top:8px;
66 | padding-left:15px;
67 | }
68 | .icon{
69 | width:30px;
70 | height:35px;
71 | display:block;
72 | float:left;
73 | }
74 | .icon-user{
75 | background:#FFF url(/static/images/one4.jpg) no-repeat center center;
76 | }
77 | .icon-pwd{
78 | background:#FFF url(/static/images/one5.jpg) no-repeat center center;
79 | }
80 | .form-input{
81 | display:block;
82 | float:left;
83 | width:236px;
84 | box-sizing: border-box;
85 | border:none;
86 | height:35px;
87 | padding: 5px ;
88 | outline: 0;
89 | }
90 | .login-btn{
91 | height:51px;
92 | width:51px;
93 | float:left;
94 | cursor:pointer;
95 | text-align:center;
96 | line-height:45px;
97 | }
98 | .slide {
99 | width: 1440px;
100 | background:#633;
101 | }
102 |
103 | .bg-top,
104 | .bg-bottom {
105 | top: 0;
106 | width: 100%;
107 | position: fixed;
108 | z-index: -15;
109 | }
110 | .bg-bottom {
111 | bottom: 0;
112 | top: auto;
113 | }
114 |
115 | .org-panel {
116 | display:none;
117 | width:220px;
118 | height:270px;
119 | float:left;
120 | left:50%;
121 | margin-left:-130px;
122 | margin-top:-200px;
123 | position:absolute;
124 | z-index:10px;
125 | overflow-y:auto;
126 | background-color:#FFF;
127 | }
128 | .org-panel ol ,li {
129 | list-style:none;
130 | padding:0px;
131 | color:#666;
132 | }
133 | .org-panel ol li {
134 | height:25px;
135 | text-indent:20px;
136 | line-height:25px;
137 | cursor:pointer;
138 | }
139 | .org-panel ol li:hover{
140 | color:#CC0033 ;
141 | }
142 |
--------------------------------------------------------------------------------
/appweb/static/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/appweb/static/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/appweb/static/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/appweb/static/images/bg-bottom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/bg-bottom.png
--------------------------------------------------------------------------------
/appweb/static/images/bg-top.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/bg-top.png
--------------------------------------------------------------------------------
/appweb/static/images/loginForm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/loginForm.png
--------------------------------------------------------------------------------
/appweb/static/images/loginb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/loginb.png
--------------------------------------------------------------------------------
/appweb/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/logo.png
--------------------------------------------------------------------------------
/appweb/static/images/one4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/one4.jpg
--------------------------------------------------------------------------------
/appweb/static/images/one5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/one5.jpg
--------------------------------------------------------------------------------
/appweb/static/images/selectOrg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/static/images/selectOrg.png
--------------------------------------------------------------------------------
/appweb/static/js/npm.js:
--------------------------------------------------------------------------------
1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
2 | require('../../js/transition.js')
3 | require('../../js/alert.js')
4 | require('../../js/button.js')
5 | require('../../js/carousel.js')
6 | require('../../js/collapse.js')
7 | require('../../js/dropdown.js')
8 | require('../../js/modal.js')
9 | require('../../js/tooltip.js')
10 | require('../../js/popover.js')
11 | require('../../js/scrollspy.js')
12 | require('../../js/tab.js')
13 | require('../../js/affix.js')
--------------------------------------------------------------------------------
/appweb/views/base/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{template "base/head.html" .}}
5 | {{template "meta" .}}
6 | {{template "addCSS" .}}
7 |
8 |
9 |
10 | {{template "top" .}}
11 |
12 | {{template "body" .}}
13 |
14 |
15 | {{template "footer" .}}
16 | {{template "addJS" .}}
17 |
18 |
--------------------------------------------------------------------------------
/appweb/views/base/base_common.html:
--------------------------------------------------------------------------------
1 | {{define "top"}}
2 | {{end}}
3 | {{define "footer"}}
4 | {{template "base/foot.html" .}}
5 | {{end}}
--------------------------------------------------------------------------------
/appweb/views/base/foot.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/appweb/views/base/foot.html
--------------------------------------------------------------------------------
/appweb/views/base/head.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/appweb/views/login.html:
--------------------------------------------------------------------------------
1 | {{template "base/base.html" .}}
2 | {{template "base/base_common.html" .}}
3 | {{define "meta"}}
4 | 有信网页版
5 | {{end}}
6 |
7 | {{define "addCSS"}}
8 |
9 | {{end}}
10 |
11 | {{define "addJS"}}
12 |
54 | {{end}}
55 |
56 | {{define "body"}}
57 |
58 |
59 |
60 |
61 | 选择单位
62 |
63 |
64 |
65 |
66 |
80 |
81 |
82 |
83 |
84 |
85 | {{range .Tenants}}
86 | - {{.Name}}
87 | {{end}}
88 |
89 |
90 |
91 |
92 | {{end}}
--------------------------------------------------------------------------------
/client/js/README.md:
--------------------------------------------------------------------------------
1 | appmsgsrv-js
2 | ======
3 |
4 | 应用消息服务 JavaScript 客户端
5 |
--------------------------------------------------------------------------------
/client/js/demo/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "html/template"
6 | "net/http"
7 | )
8 |
9 | func index(w http.ResponseWriter, r *http.Request) {
10 | t, _ := template.ParseFiles("template/demo.html")
11 |
12 | t.Execute(w, nil)
13 | }
14 |
15 | func main() {
16 | fmt.Println("启动 Demo 应用")
17 |
18 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
19 | http.HandleFunc("/", index)
20 | http.ListenAndServe(":8310", nil)
21 | }
22 |
--------------------------------------------------------------------------------
/client/js/demo/static/ws-flash/WebSocketMain.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/client/js/demo/static/ws-flash/WebSocketMain.swf
--------------------------------------------------------------------------------
/client/js/demo/template/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Go Push Cluster JavaScript Demo
6 |
7 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/client/js/flash-security-policy-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "net"
7 | "os"
8 | "os/signal"
9 | "strconv"
10 | "syscall"
11 | )
12 |
13 | var (
14 | socketpolicy []byte
15 | port int = 843
16 | socketpolicyfile string = "socketpolicy.xml"
17 | )
18 |
19 | func signal_arm() {
20 | fmt.Println("signal armed")
21 |
22 | go func() {
23 | for {
24 | c := make(chan os.Signal)
25 | signal.Notify(c, syscall.SIGINT)
26 | sig := <-c
27 |
28 | fmt.Println("%s", sig.String())
29 |
30 | switch sig {
31 | case syscall.SIGINT:
32 | os.Exit(0)
33 | default:
34 | //os.Exit(-1)
35 | }
36 | }
37 | }()
38 | }
39 |
40 | func loadPolicyFile() bool {
41 | file, err := os.Open(socketpolicyfile)
42 | if err == nil {
43 | stat, _ := file.Stat()
44 | socketpolicy = make([]byte, stat.Size()+1)
45 | file.Read(socketpolicy)
46 | socketpolicy[stat.Size()] = 0
47 | file.Close()
48 | file = nil
49 | } else {
50 | fmt.Println(">>>> socket policy file open error:", err.Error())
51 | return false
52 | }
53 | return true
54 | }
55 |
56 | func main() {
57 | signal_arm()
58 |
59 | // Parse args
60 | flag.IntVar(&port, "port", port, "listen port number")
61 | flag.StringVar(&socketpolicyfile, "file", socketpolicyfile, "socket policy file name")
62 | flag.Parse()
63 |
64 | fmt.Println("config listen port =", port)
65 | fmt.Println("config socket policy =", socketpolicyfile)
66 |
67 | if loadPolicyFile() == false {
68 | return
69 | }
70 |
71 | accepts()
72 | }
73 |
74 | func accepts() {
75 | strPort := "0.0.0.0:" + strconv.Itoa(port)
76 | fmt.Println("open port", strPort)
77 | addr, err := net.ResolveTCPAddr("tcp4", strPort)
78 | l, err := net.ListenTCP("tcp4", addr)
79 | if err != nil {
80 | fmt.Println(">>>> listen failed: ", err.Error())
81 | return
82 | }
83 | addr = nil
84 | for {
85 | fmt.Println("Accept ready")
86 | session, err := l.AcceptTCP()
87 | if err != nil {
88 | //return
89 | fmt.Println("Accept error:", err.Error())
90 | continue
91 | }
92 |
93 | go session_process(session)
94 | session = nil
95 | }
96 |
97 | l = nil
98 | }
99 |
100 | func session_process(s *net.TCPConn) {
101 | fmt.Println("Accepted session start")
102 | if recieve_request(s) {
103 | send_response(s)
104 | }
105 | s.Close()
106 | s = nil
107 | fmt.Println("session done")
108 | }
109 |
110 | func recieve_request(s *net.TCPConn) bool {
111 | // 超適当なのでリクエストの受信完了は待たない
112 | /*
113 | _, err := ioutil.ReadAll(s)
114 | if err == io.EOF {
115 | // write request validation here!
116 | return true
117 | }
118 | return false
119 | */
120 |
121 | return true
122 | }
123 |
124 | func send_response(s *net.TCPConn) {
125 | s.Write(socketpolicy)
126 | }
127 |
--------------------------------------------------------------------------------
/client/js/flash-security-policy-server/socketpolicy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/comet.ini:
--------------------------------------------------------------------------------
1 | [program:gopush-comet]
2 | command=/home/paas/paas/appmsgsrv/bin/comet -c /home/paas/paas/appmsgsrv/bin/comet.conf -dbc /home/paas/paas/appmsgsrv/bin/db.conf -log_level=info
3 |
4 | ;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
5 | numprocs=1 ; number of processes copies to start (def 1)
6 | directory=/home/paas/paas/appmsgsrv/bin ; directory to cwd to before exec (def no cwd)
7 | ;umask=022 ; umask for process (default None)
8 | ;priority=999 ; the relative start priority (default 999)
9 | autostart=true ; start at supervisord start (default: true)
10 | autorestart=true ; whether/when to restart (default: unexpected)
11 | startsecs=10 ; number of secs prog must stay running (def. 1)
12 | startretries=10 ; max # of serial start failures (default 3)
13 | ;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)
14 | ;stopsignal=QUIT ; signal used to kill process (default TERM)
15 | ;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
16 | ;stopasgroup=false ; send stop signal to the UNIX process group (default false)
17 | ;killasgroup=false ; SIGKILL the UNIX process group (def false)
18 | user=root ; setuid to this UNIX account to run the program
19 | redirect_stderr=true ; redirect proc stderr to stdout (default false)
20 | stdout_logfile=/home/paas/paas/appmsgsrv/logs/comet/comet.log ; stdout log path, NONE for none; default AUTO
21 | stdout_logfile_maxbytes=50MB ; max # logfile bytes b4 rotation (default 50MB)
22 | stdout_logfile_backups=10 ; # of stdout logfile backups (default 10)
23 | stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
24 | ;stdout_events_enabled=false ; emit events on stdout writes (default false)
25 | ;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO
26 | ;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
27 | ;stderr_logfile_backups=10 ; # of stderr logfile backups (default 10)
28 | ;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
29 | ;stderr_events_enabled=false ; emit events on stderr writes (default false)
30 | ;environment=A=1,B=2 ; process environment additions (def no adds)
31 | ;serverurl=AUTO ; override serverurl computation (childutils)
32 |
--------------------------------------------------------------------------------
/comet/config.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "flag"
21 | "github.com/Terry-Mao/goconf"
22 | "runtime"
23 | "time"
24 | )
25 |
26 | var (
27 | Conf *Config
28 | confFile string
29 | )
30 |
31 | func init() {
32 | flag.StringVar(&confFile, "c", "./comet.conf", " set gopush-cluster comet config file path")
33 | }
34 |
35 | type Config struct {
36 | // base
37 | User string `goconf:"base:user"`
38 | PidFile string `goconf:"base:pidfile"`
39 | Dir string `goconf:"base:dir"`
40 | MaxProc int `goconf:"base:maxproc"`
41 | TCPBind []string `goconf:"base:tcp.bind:,"`
42 | WebsocketBind []string `goconf:"base:websocket.bind:,"`
43 | RPCBind []string `goconf:"base:rpc.bind:,"`
44 | PprofBind []string `goconf:"base:pprof.bind:,"`
45 | StatBind []string `goconf:"base:stat.bind:,"`
46 | // zookeeper
47 | ZookeeperAddr []string `goconf:"zookeeper:addr:,"`
48 | ZookeeperTimeout time.Duration `goconf:"zookeeper:timeout:time"`
49 | ZookeeperCometPath string `goconf:"zookeeper:comet.path"`
50 | ZookeeperCometNode string `goconf:"zookeeper:comet.node"`
51 | ZookeeperMessagePath string `goconf:"zookeeper:message.path"`
52 | // rpc
53 | RPCPing time.Duration `goconf:"rpc:ping:time"`
54 | RPCRetry time.Duration `goconf:"rpc:retry:time"`
55 | // channel
56 | SndbufSize int `goconf:"channel:sndbuf.size:memory"`
57 | RcvbufSize int `goconf:"channel:rcvbuf.size:memory"`
58 | Proto []string `goconf:"channel:proto:,"`
59 | BufioInstance int `goconf:"channel:bufio.instance"`
60 | BufioNum int `goconf:"channel:bufio.num"`
61 | TCPKeepalive bool `goconf:"channel:tcp.keepalive"`
62 | MaxSubscriberPerChannel int `goconf:"channel:maxsubscriber"`
63 | ChannelBucket int `goconf:"channel:bucket"`
64 | Auth bool `goconf:"channel:auth"`
65 | TokenExpire time.Duration `goconf:"-"`
66 | MsgBufNum int `goconf:"channel:msgbuf.num"`
67 | }
68 |
69 | // InitConfig get a new Config struct.
70 | func InitConfig() error {
71 | Conf = &Config{
72 | // base
73 | User: "nobody nobody",
74 | PidFile: "/tmp/gopush-cluster-comet.pid",
75 | Dir: "./",
76 | MaxProc: runtime.NumCPU(),
77 | WebsocketBind: []string{"localhost:6968"},
78 | TCPBind: []string{"localhost:6969"},
79 | RPCBind: []string{"localhost:6970"},
80 | PprofBind: []string{"localhost:6971"},
81 | StatBind: []string{"localhost:6972"},
82 | // zookeeper
83 | ZookeeperAddr: []string{"localhost:2181"},
84 | ZookeeperTimeout: 30 * time.Second,
85 | ZookeeperCometPath: "/gopush-cluster-comet",
86 | ZookeeperCometNode: "node1",
87 | ZookeeperMessagePath: "/gopush-cluster-message",
88 | // rpc
89 | RPCPing: 1 * time.Second,
90 | RPCRetry: 1 * time.Second,
91 | // channel
92 | SndbufSize: 2048,
93 | RcvbufSize: 256,
94 | Proto: []string{"tcp", "websocket"},
95 | BufioInstance: runtime.NumCPU(),
96 | BufioNum: 128,
97 | TCPKeepalive: false,
98 | TokenExpire: 30 * 24 * time.Hour,
99 | MaxSubscriberPerChannel: 64,
100 | ChannelBucket: runtime.NumCPU(),
101 | Auth: false,
102 | MsgBufNum: 30,
103 | }
104 | c := goconf.New()
105 | if err := c.Parse(confFile); err != nil {
106 | logger.Errorf("goconf.Parse(\"%s\") error(%v)", confFile, err)
107 | return err
108 | }
109 | if err := c.Unmarshal(Conf); err != nil {
110 | logger.Errorf("goconf.Unmarshall() error(%v)", err)
111 | return err
112 | }
113 | return nil
114 | }
115 |
--------------------------------------------------------------------------------
/comet/db.conf:
--------------------------------------------------------------------------------
1 | [base]
2 | #Mysql configuration
3 | app.dbURL root:123456@tcp(10.180.120.63:3308)/appmsgsrv?parseTime=true
4 | app.dbMaxIdleConns 10
5 | app.dbMaxOpenConns 100
--------------------------------------------------------------------------------
/comet/main.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "flag"
21 | "github.com/EPICPaaS/appmsgsrv/db"
22 | "github.com/EPICPaaS/appmsgsrv/perf"
23 | "github.com/EPICPaaS/appmsgsrv/process"
24 | "github.com/EPICPaaS/appmsgsrv/ver"
25 | "github.com/b3log/wide/log"
26 | "os"
27 | "runtime"
28 | "time"
29 | )
30 |
31 | var logger = log.NewLogger(os.Stdout)
32 |
33 | func main() {
34 |
35 | logLevel := flag.String("log_level", "info", "logger level")
36 | // parse cmd-line arguments
37 | flag.Parse()
38 | log.SetLevel(*logLevel)
39 | logger.Infof("comet ver: \"%s\" start", ver.Version)
40 |
41 | // init config
42 | if err := InitConfig(); err != nil {
43 | logger.Errorf("InitConfig() error(%v)", err)
44 | return
45 | }
46 | //init db config
47 | if err := db.InitConfig(); err != nil {
48 | logger.Error("db-InitConfig() wrror(%v)", err)
49 | return
50 | }
51 |
52 | db.InitDB()
53 | defer db.CloseDB()
54 |
55 | // set max routine
56 | runtime.GOMAXPROCS(Conf.MaxProc)
57 | // start pprof
58 | perf.Init(Conf.PprofBind)
59 | // create channel
60 | // if process exit, close channel
61 | UserChannel = NewChannelList()
62 | defer UserChannel.Close()
63 | // start stats
64 | StartStats()
65 | // start rpc
66 | StartRPC()
67 | // start comet
68 | StartComet()
69 | // init zookeeper
70 | zkConn, err := InitZK()
71 | if err != nil {
72 | logger.Errorf("InitZookeeper() error(%v)", err)
73 | return
74 | }
75 | // if process exit, close zk
76 | defer zkConn.Close()
77 | // init process
78 | // sleep one second, let the listen start
79 | time.Sleep(time.Second)
80 | if err = process.Init(Conf.User, Conf.Dir, Conf.PidFile); err != nil {
81 | logger.Errorf("process.Init(\"%s\", \"%s\", \"%s\") error(%v)", Conf.User, Conf.Dir, Conf.PidFile, err)
82 | return
83 | }
84 | // init signals, block wait signals
85 | signalCH := InitSignal()
86 | HandleSignal(signalCH)
87 | // exit
88 | logger.Info("comet stop")
89 | }
90 |
--------------------------------------------------------------------------------
/comet/pubsub.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "errors"
21 | "time"
22 | )
23 |
24 | const (
25 | TCPProto = uint8(0)
26 | WebsocketProto = uint8(1)
27 | WebsocketProtoStr = "websocket"
28 | TCPProtoStr = "tcp"
29 | Heartbeat = "h"
30 | minHearbeatSec = 30
31 | delayHeartbeatSec = 5
32 | fitstPacketTimedoutSec = time.Second * 5
33 | Second = int64(time.Second)
34 | )
35 |
36 | var (
37 | // Exceed the max subscriber per key
38 | ErrMaxConn = errors.New("Exceed the max subscriber connection per key")
39 | // Assection type failed
40 | ErrAssertType = errors.New("Subscriber assert type failed")
41 | // Heartbeat
42 | // HeartbeatLen = len(Heartbeat)
43 | // hearbeat
44 | HeartbeatReply = []byte("+h\r\n")
45 | // auth failed reply
46 | AuthReply = []byte("-a\r\n")
47 | // channle not found reply
48 | ChannelReply = []byte("-c\r\n")
49 | // param error reply
50 | ParamReply = []byte("-p\r\n")
51 | )
52 |
53 | // StartListen start accept client.
54 | func StartComet() {
55 | for _, proto := range Conf.Proto {
56 | if proto == WebsocketProtoStr {
57 | // Start http push service
58 | StartHttp()
59 | } else if proto == TCPProtoStr {
60 | // Start tcp push service
61 | StartTCP()
62 | } else {
63 | logger.Warnf("unknown gopush-cluster protocol %s, (\"websocket\" or \"tcp\")", proto)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/comet/signal.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "os"
21 | "os/signal"
22 | "syscall"
23 | )
24 |
25 | // InitSignal register signals handler.
26 | func InitSignal() chan os.Signal {
27 | c := make(chan os.Signal, 1)
28 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGSTOP)
29 | return c
30 | }
31 |
32 | // HandleSignal fetch signal from chan then do exit or reload.
33 | func HandleSignal(c chan os.Signal) {
34 | // Block until a signal is received.
35 | for {
36 | s := <-c
37 | logger.Tracef("comet get a signal %s", s.String())
38 | switch s {
39 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT:
40 | logger.Error("Comet Exit: syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT")
41 | return
42 | case syscall.SIGHUP:
43 | // TODO reload
44 | //return
45 | logger.Error("Comet Exit: syscall.SIGHUP")
46 | default:
47 | logger.Error("Comet Exit: default")
48 | return
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/comet/token.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "container/list"
21 | "errors"
22 | "time"
23 | )
24 |
25 | var (
26 | // Token exists
27 | ErrTokenExist = errors.New("token exist")
28 | // Token not exists
29 | ErrTokenNotExist = errors.New("token not exist")
30 | // Token expired
31 | ErrTokenExpired = errors.New("token expired")
32 | )
33 |
34 | // Token struct
35 | type Token struct {
36 | token map[string]*list.Element // token map
37 | lru *list.List // lru double linked list
38 | }
39 |
40 | // Token Element
41 | type TokenData struct {
42 | Ticket string
43 | Expire time.Time
44 | }
45 |
46 | // NewToken create a token struct ptr
47 | func NewToken() *Token {
48 | return &Token{
49 | token: map[string]*list.Element{},
50 | lru: list.New(),
51 | }
52 | }
53 |
54 | // Add add a token
55 | func (t *Token) Add(ticket string) error {
56 | if e, ok := t.token[ticket]; !ok {
57 | // new element add to lru back
58 | e = t.lru.PushBack(&TokenData{Ticket: ticket, Expire: time.Now().Add(Conf.TokenExpire)})
59 | t.token[ticket] = e
60 | } else {
61 | logger.Warnf("token \"%s\" exist", ticket)
62 | return ErrTokenExist
63 | }
64 | t.clean()
65 | return nil
66 | }
67 |
68 | // Auth auth a token is valid
69 | func (t *Token) Auth(ticket string) error {
70 | if e, ok := t.token[ticket]; !ok {
71 | logger.Warnf("token \"%s\" not exist", ticket)
72 | return ErrTokenNotExist
73 | } else {
74 | td, _ := e.Value.(*TokenData)
75 | if time.Now().After(td.Expire) {
76 | t.clean()
77 | logger.Warnf("token \"%s\" expired", ticket)
78 | return ErrTokenExpired
79 | }
80 | td.Expire = time.Now().Add(Conf.TokenExpire)
81 | t.lru.MoveToBack(e)
82 | }
83 | t.clean()
84 | return nil
85 | }
86 |
87 | // clean scan the lru list expire the element
88 | func (t *Token) clean() {
89 | now := time.Now()
90 | e := t.lru.Front()
91 | for {
92 | if e == nil {
93 | break
94 | }
95 | td, _ := e.Value.(*TokenData)
96 | if now.After(td.Expire) {
97 | logger.Warnf("token \"%s\" expired", td.Ticket)
98 | o := e.Next()
99 | delete(t.token, td.Ticket)
100 | t.lru.Remove(e)
101 | e = o
102 | continue
103 | }
104 | break
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/comet/zk.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | // github.com/samuel/go-zookeeper
18 | // Copyright (c) 2013, Samuel Stauffer
19 | // All rights reserved.
20 |
21 | package main
22 |
23 | import (
24 | "fmt"
25 | "github.com/EPICPaaS/appmsgsrv/rpc"
26 | myzk "github.com/EPICPaaS/appmsgsrv/zk"
27 | "github.com/samuel/go-zookeeper/zk"
28 | "path"
29 | "strings"
30 | )
31 |
32 | func InitZK() (*zk.Conn, error) {
33 | conn, err := myzk.Connect(Conf.ZookeeperAddr, Conf.ZookeeperTimeout)
34 | if err != nil {
35 | logger.Errorf("zk.Connect() error(%v)", err)
36 | return nil, err
37 | }
38 | fpath := path.Join(Conf.ZookeeperCometPath, Conf.ZookeeperCometNode)
39 | if err = myzk.Create(conn, fpath); err != nil {
40 | logger.Errorf("zk.Create() error(%v)", err)
41 | return conn, err
42 | }
43 | // tcp, websocket and rpc bind address store in the zk
44 | data := ""
45 | for _, addr := range Conf.TCPBind {
46 | data += fmt.Sprintf("tcp://%s,", addr)
47 | }
48 | for _, addr := range Conf.WebsocketBind {
49 | data += fmt.Sprintf("ws://%s,", addr)
50 | }
51 | for _, addr := range Conf.RPCBind {
52 | data += fmt.Sprintf("rpc://%s,", addr)
53 | }
54 | data = strings.TrimRight(data, ",")
55 | logger.Tracef("zk data: \"%s\"", data)
56 | if err = myzk.RegisterTemp(conn, fpath, data); err != nil {
57 | logger.Errorf("zk.RegisterTemp() error(%v)", err)
58 | return conn, err
59 | }
60 | rpc.InitMessage(conn, Conf.ZookeeperMessagePath, Conf.RPCRetry, Conf.RPCPing)
61 | return conn, nil
62 | }
63 |
--------------------------------------------------------------------------------
/db/config.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package db
18 |
19 | import (
20 | "flag"
21 | "fmt"
22 | "github.com/Terry-Mao/goconf"
23 | "github.com/b3log/wide/log"
24 | "os"
25 | )
26 |
27 | var (
28 | Conf *Config
29 | confFile string
30 | )
31 |
32 | var logger = log.NewLogger(os.Stdout)
33 |
34 | // InitConfig initialize config file path
35 | func init() {
36 | flag.StringVar(&confFile, "dbc", "./db.conf", " set db config file path")
37 | }
38 |
39 | type Config struct {
40 | AppDBURL string `goconf:"base:app.dbURL"`
41 | AppDBMaxIdleConns int `goconf:"base:app.dbMaxIdleConns"`
42 | AppDBMaxOpenConns int `goconf:"base:app.dbMaxOpenConns"`
43 | }
44 |
45 | // InitConfig init configuration file.
46 | func InitConfig() error {
47 |
48 | gconf := goconf.New()
49 | if err := gconf.Parse(confFile); err != nil {
50 | fmt.Println("confFile", confFile)
51 | logger.Errorf("goconf.Parse(\"%s\") error(%v)", confFile, err)
52 | return err
53 | }
54 | Conf = &Config{}
55 | if err := gconf.Unmarshal(Conf); err != nil {
56 | logger.Errorf("goconf.Unmarshall() error(%v)", err)
57 | return err
58 | }
59 |
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/db/db.conf:
--------------------------------------------------------------------------------
1 | [base]
2 | #Mysql configuration
3 | app.dbURL root:123456@tcp(10.180.120.63:3308)/appmsgsrv?parseTime=true
4 | app.dbMaxIdleConns 10
5 | app.dbMaxOpenConns 100
--------------------------------------------------------------------------------
/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "database/sql"
5 |
6 | _ "github.com/go-sql-driver/mysql"
7 | )
8 |
9 | // 数据库操作句柄.
10 | var MySQL *sql.DB
11 |
12 | // 初始化数据库连接.
13 | func InitDB() {
14 | logger.Info("Connecting DB....")
15 |
16 | var err error
17 | MySQL, err = sql.Open("mysql", Conf.AppDBURL)
18 |
19 | if nil != err {
20 | logger.Error(err)
21 | }
22 |
23 | // 实际测试一次
24 | test := 0
25 | if err := MySQL.QueryRow("SELECT 1").Scan(&test); err != nil {
26 | logger.Error(err)
27 | }
28 |
29 | logger.Infof("DB max idle conns [%d]", Conf.AppDBMaxIdleConns)
30 | logger.Infof("DB max open conns [%d]", Conf.AppDBMaxOpenConns)
31 |
32 | MySQL.SetMaxIdleConns(Conf.AppDBMaxIdleConns)
33 | MySQL.SetMaxOpenConns(Conf.AppDBMaxOpenConns)
34 |
35 | logger.Info("DB connected")
36 | }
37 |
38 | // 关闭数据库连接.
39 | func CloseDB() {
40 | MySQL.Close()
41 | }
42 |
--------------------------------------------------------------------------------
/dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Dependencies
3 |
4 | # go get -u github.com/Terry-Mao/gopush-cluster
5 | # go get -u github.com/Terry-Mao/goconf
6 | # go get -u github.com/garyburd/redigo/redis
7 | # go get -u code.google.com/p/go.net/websocket
8 | # go get -u github.com/samuel/go-zookeeper
9 | # go get -u github.com/golang/glog
10 |
11 | go get -u ./message/...
12 | go get -u ./comet/...
13 | go get -u ./web/...
14 |
--------------------------------------------------------------------------------
/hash/ketama.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package hash
18 |
19 | import (
20 | // "crypto/md5"
21 | "fmt"
22 | "sort"
23 | )
24 |
25 | // Convenience types for common cases
26 | // UIntSlice attaches the methods of Interface to []uint, sorting in increasing order.
27 | type UIntSlice []uint
28 |
29 | func (p UIntSlice) Len() int { return len(p) }
30 | func (p UIntSlice) Less(i, j int) bool { return p[i] < p[j] }
31 | func (p UIntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
32 |
33 | type Ketama struct {
34 | node int // phsical node number
35 | vnode int // virual node number
36 | nodes []uint // nodes
37 | nodesMapping map[uint]string // nodes maping
38 | }
39 |
40 | // New create a ketama consistent hashing struct
41 | func NewKetama(node, vnode int) *Ketama {
42 | ketama := &Ketama{}
43 | ketama.node = node
44 | ketama.vnode = vnode
45 | ketama.nodes = []uint{}
46 | ketama.nodesMapping = map[uint]string{}
47 |
48 | ketama.initCircle()
49 |
50 | return ketama
51 | }
52 |
53 | // New create a ketama consistent hashing struct use exist node slice
54 | func NewKetama2(node []string, vnode int) *Ketama {
55 | ketama := &Ketama{}
56 | ketama.node = len(node)
57 | ketama.vnode = vnode
58 | ketama.nodes = []uint{}
59 | ketama.nodesMapping = map[uint]string{}
60 |
61 | ketama.initCircle2(node)
62 |
63 | return ketama
64 | }
65 |
66 | // init consistent hashing circle
67 | func (k *Ketama) initCircle() {
68 | h := NewMurmur3C()
69 | for idx := 1; idx < k.node+1; idx++ {
70 | node := fmt.Sprintf("node%d", idx)
71 | for i := 0; i < k.vnode; i++ {
72 | vnode := fmt.Sprintf("%s#%d", node, i)
73 | h.Write([]byte(vnode))
74 | vpos := uint(h.Sum32())
75 | k.nodes = append(k.nodes, vpos)
76 | k.nodesMapping[vpos] = node
77 | h.Reset()
78 | }
79 | }
80 |
81 | sort.Sort(UIntSlice(k.nodes))
82 | }
83 |
84 | // init consistent hashing circle
85 | func (k *Ketama) initCircle2(node []string) {
86 | h := NewMurmur3C()
87 | for _, str := range node {
88 | for i := 0; i < k.vnode; i++ {
89 | vnode := fmt.Sprintf("%s#%d", str, i)
90 | h.Write([]byte(vnode))
91 | vpos := uint(h.Sum32())
92 | k.nodes = append(k.nodes, vpos)
93 | k.nodesMapping[vpos] = str
94 | h.Reset()
95 | }
96 | }
97 |
98 | sort.Sort(UIntSlice(k.nodes))
99 | }
100 |
101 | // Node get a consistent hashing node by key
102 | func (k *Ketama) Node(key string) string {
103 | if len(k.nodes) == 0 {
104 | return ""
105 | }
106 | h := NewMurmur3C()
107 | h.Write([]byte(key))
108 | idx := searchLeft(k.nodes, uint(h.Sum32()))
109 | pos := k.nodes[0]
110 | if idx != len(k.nodes) {
111 | pos = k.nodes[idx]
112 | }
113 |
114 | return k.nodesMapping[pos]
115 | }
116 |
117 | // search the slice left-most value
118 | func searchLeft(a []uint, x uint) int {
119 | lo := 0
120 | hi := len(a)
121 | for lo < hi {
122 | mid := (lo + hi) / 2
123 | if a[mid] < x {
124 | lo = mid + 1
125 | } else {
126 | hi = mid
127 | }
128 | }
129 |
130 | return lo
131 | }
132 |
--------------------------------------------------------------------------------
/hash/ketama_test.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package hash
18 |
19 | import (
20 | "testing"
21 | )
22 |
23 | func TestKetama(t *testing.T) {
24 | k := NewKetama(15, 255)
25 | n := k.Node("Terry-Mao332")
26 | if n != "node15" {
27 | t.Error("Terry-Mao332 must hit node13")
28 | }
29 |
30 | n = k.Node("Terry-Mao2")
31 | if n != "node13" {
32 | t.Error("Terry-Mao2 must hit node13")
33 | }
34 |
35 | n = k.Node("Terry-Mao3")
36 | if n != "node5" {
37 | t.Error("Terry-Mao3 must hit node13")
38 | }
39 |
40 | n = k.Node("Terry-Mao4")
41 | if n != "node2" {
42 | t.Error("Terry-Mao4 must hit node13")
43 | }
44 |
45 | n = k.Node("Terry-Mao5")
46 | if n != "node6" {
47 | t.Error("Terry-Mao5 must hit node13")
48 | }
49 | }
50 |
51 | func TestKetama2(t *testing.T) {
52 | k := NewKetama2([]string{"11", "22"}, 255)
53 | n := k.Node("Terry-Mao332")
54 | if n != "22" {
55 | t.Error("Terry-Mao332 must hit 22")
56 | }
57 |
58 | n = k.Node("Terry-Mao333")
59 | if n != "22" {
60 | t.Error("Terry-Mao333 must hit 22")
61 | }
62 |
63 | n = k.Node("Terry-Mao334")
64 | if n != "22" {
65 | t.Error("Terry-Mao333 must hit 22")
66 | }
67 |
68 | n = k.Node("Terry-Mao335")
69 | if n != "11" {
70 | t.Error("Terry-Mao335 must hit 11")
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/hash/mmhash3_test.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 The Go Authors. All rights reserved.
2 | // refer https://github.com/gwenn/murmurhash3
3 |
4 | package hash
5 |
6 | import (
7 | "hash"
8 | "testing"
9 | )
10 |
11 | const testDataSize = 40
12 |
13 | func TestMurmur3A(t *testing.T) {
14 | expected := uint32(3127628307)
15 | hash := Murmur3A([]byte("test"), 0)
16 | if hash != expected {
17 | t.Errorf("Expected %d but was %d for Murmur3A\n", expected, hash)
18 | }
19 | }
20 |
21 | func TestMurmur3C(t *testing.T) {
22 | expected := []uint32{1862463280, 1426881896, 1426881896, 1426881896}
23 | hash := Murmur3C([]byte("test"), 0)
24 | for i, e := range expected {
25 | if hash[i] != e {
26 | t.Errorf("Expected %d but was %d for Murmur3C[%d]\n", e, hash[i], i)
27 | }
28 | }
29 | }
30 |
31 | func TestMurmur3F(t *testing.T) {
32 | expected := []uint64{12429135405209477533, 11102079182576635266}
33 | hash := Murmur3F([]byte("test"), 0)
34 | for i, e := range expected {
35 | if hash[i] != e {
36 | t.Errorf("Expected %d but was %d for Murmur3F[%d]\n", e, hash[i], i)
37 | }
38 | }
39 | }
40 |
41 | func Benchmark3A(b *testing.B) {
42 | benchmark(b, NewMurmur3A())
43 | }
44 | func Benchmark3C(b *testing.B) {
45 | benchmark(b, NewMurmur3C())
46 | }
47 | func Benchmark3F(b *testing.B) {
48 | benchmark(b, NewMurmur3F())
49 | }
50 |
51 | func benchmark(b *testing.B, h hash.Hash) {
52 | b.ResetTimer()
53 | b.SetBytes(testDataSize)
54 | data := make([]byte, testDataSize)
55 | for i := range data {
56 | data[i] = byte(i + 'a')
57 | }
58 |
59 | b.StartTimer()
60 | for todo := b.N; todo != 0; todo-- {
61 | h.Reset()
62 | h.Write(data)
63 | h.Sum(nil)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/hlist/hlist.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package hlist
18 |
19 | // Head is an head of a linked hlist.
20 | type Head struct {
21 | first *Element
22 | }
23 |
24 | // Element is an element of a linked hlist.
25 | type Element struct {
26 | next *Element
27 | pprev **Element
28 |
29 | // The value stored with this element.
30 | Value interface{}
31 | }
32 |
33 | // Next returns the next hlist element or nil.
34 | func (e *Element) Next() *Element {
35 | return e.next
36 | }
37 |
38 | // Hlist represents a doubly linked hlist.
39 | // The zero value for Hlist is an empty Hlist ready to use.
40 | type Hlist struct {
41 | root Head // sentinel hlist head
42 | len int // current hlist length excluding (this) sentinel element
43 | }
44 |
45 | // Init initializes or clears hlist l.
46 | func (l *Hlist) Init() *Hlist {
47 | l.root.first = nil
48 | l.len = 0
49 | return l
50 | }
51 |
52 | // New returns an initialized hlist.
53 | func New() *Hlist { return new(Hlist).Init() }
54 |
55 | // Len returns the number of elements of hlist l.
56 | // The complexity is O(1).
57 | func (l *Hlist) Len() int { return l.len }
58 |
59 | // Front returns the first element of hlist l or nil
60 | func (l *Hlist) Front() *Element {
61 | return l.root.first
62 | }
63 |
64 | // PushFront inserts a new element e with value v at the front of hlist l and returns e.
65 | func (l *Hlist) PushFront(v interface{}) *Element {
66 | first := l.root.first
67 | n := &Element{Value: v}
68 | n.next = first
69 | if first != nil {
70 | first.pprev = &n.next
71 | }
72 | l.root.first = n
73 | n.pprev = &l.root.first
74 | l.len++
75 | return n
76 | }
77 |
78 | // Remove removes e from l if e is an element of hlist l.
79 | // It returns the element value e.Value.
80 | func (l *Hlist) Remove(e *Element) interface{} {
81 | next := e.next
82 | pprev := e.pprev
83 | *pprev = next
84 | if next != nil {
85 | next.pprev = pprev
86 | }
87 | l.len--
88 | e.next = nil // avoid memory leak
89 | e.pprev = nil // avoid memory leak
90 | return e.Value
91 | }
92 |
--------------------------------------------------------------------------------
/hlist/hlist_test.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package hlist
18 |
19 | import (
20 | "fmt"
21 | "testing"
22 | )
23 |
24 | func TestHlist(t *testing.T) {
25 | l := New()
26 | first := l.Front()
27 | if first != nil {
28 | t.Error("first != nil")
29 | }
30 | l.PushFront(1)
31 | l.PushFront(2)
32 | first = l.Front()
33 | if first == nil {
34 | t.Error("first == nil")
35 | }
36 | if i, ok := first.Value.(int); !ok {
37 | t.Error("first.Value assection failed")
38 | } else {
39 | if i != 2 {
40 | t.Errorf("i value error: %d", i)
41 | }
42 | }
43 | if next := first.Next(); next == nil {
44 | t.Error("next == nil")
45 | } else {
46 | if i, ok := next.Value.(int); !ok {
47 | t.Error("next.Value assection failed")
48 | } else {
49 | if i != 1 {
50 | t.Errorf("i value error: %d", i)
51 | }
52 | }
53 | }
54 | if l.Len() != 2 {
55 | t.Errorf("length error")
56 | }
57 | l.PushFront(3)
58 | l.PushFront(4)
59 | l.PushFront(5)
60 | l.PushFront(6)
61 | first = l.Front()
62 | if l.Len() != 6 {
63 | t.Errorf("length error")
64 | }
65 | for e := l.Front(); e != nil; e = e.Next() {
66 | if i, ok := e.Value.(int); !ok {
67 | t.Error("e.Value assection failed")
68 | } else {
69 | fmt.Println(i)
70 | }
71 | }
72 | fmt.Println("------")
73 | if i, ok := l.Remove(first).(int); !ok {
74 | t.Error("first.Value assection failed")
75 | } else {
76 | if i != 6 {
77 | t.Errorf("i value error: %d", i)
78 | }
79 | }
80 | if l.Len() != 5 {
81 | t.Errorf("length error")
82 | }
83 | for e := l.Front(); e != nil; e = e.Next() {
84 | if i, ok := e.Value.(int); !ok {
85 | t.Error("e.Value assection failed")
86 | } else {
87 | fmt.Println(i)
88 | }
89 | }
90 | second := l.Front() // 5
91 | thrid := second.Next() // 4
92 | fourth := thrid.Next() // 3
93 | fifth := fourth.Next() // 2
94 | sixth := fifth.Next() // 1
95 | l.Remove(second)
96 | l.Remove(thrid)
97 | l.Remove(fourth)
98 | l.Remove(fifth)
99 | l.Remove(sixth)
100 | if l.Len() != 0 {
101 | t.Errorf("length error")
102 | }
103 | first = l.Front()
104 | if first != nil {
105 | t.Error("first != nil")
106 | }
107 | for e := l.Front(); e != nil; e = e.Next() {
108 | if i, ok := e.Value.(int); !ok {
109 | t.Error("e.Value assection failed")
110 | } else {
111 | fmt.Println(i)
112 | }
113 | }
114 |
115 | e := l.PushFront(7)
116 | if i, ok := e.Value.(int); !ok {
117 | t.Error("e.Value assection failed")
118 | } else {
119 | if i != 7 {
120 | t.Error("i value error: %d", i)
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/id/timeid.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package id
18 |
19 | import (
20 | "time"
21 | )
22 |
23 | type TimeID struct {
24 | lastID int64
25 | }
26 |
27 | // NewTimeID create a new TimeID struct
28 | func NewTimeID() *TimeID {
29 | return &TimeID{lastID: 0}
30 | }
31 |
32 | // ID generate a time ID
33 | func (t *TimeID) ID() int64 {
34 | for {
35 | s := time.Now().UnixNano() / 1000
36 | if t.lastID >= s {
37 | // if last time id > current time, may be who change the system id,
38 | // so sleep last time id minus current time
39 | time.Sleep(time.Duration((t.lastID-s+1)*1000) * time.Nanosecond)
40 | } else {
41 | // save the current time id
42 | t.lastID = s
43 | return s
44 | }
45 | }
46 | return 0
47 | }
48 |
--------------------------------------------------------------------------------
/id/timeid_test.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package id
18 |
19 | import (
20 | "testing"
21 | )
22 |
23 | func TestTimeID(t *testing.T) {
24 | tid := NewTimeID()
25 | a := tid.ID()
26 | b := tid.ID()
27 | if a > b {
28 | t.Error("time a > b")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ketama/ketama.go:
--------------------------------------------------------------------------------
1 | /*
2 | refer https://github.com/mncaudill/ketama
3 |
4 | The MIT License (MIT)
5 |
6 | Copyright (c) 2013 Nolan Caudill
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of
9 | this software and associated documentation files (the "Software"), to deal in
10 | the Software without restriction, including without limitation the rights to
11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12 | the Software, and to permit persons to whom the Software is furnished to do so,
13 | subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 | */
25 | package ketama
26 |
27 | import (
28 | "crypto/sha1"
29 | "sort"
30 | "strconv"
31 | )
32 |
33 | type node struct {
34 | node string
35 | hash uint
36 | }
37 |
38 | type tickArray []node
39 |
40 | func (p tickArray) Len() int { return len(p) }
41 | func (p tickArray) Less(i, j int) bool { return p[i].hash < p[j].hash }
42 | func (p tickArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
43 | func (p tickArray) Sort() { sort.Sort(p) }
44 |
45 | type HashRing struct {
46 | defaultSpots int
47 | ticks tickArray
48 | length int
49 | }
50 |
51 | func NewRing(n int) (h *HashRing) {
52 | h = new(HashRing)
53 | h.defaultSpots = n
54 | return
55 | }
56 |
57 | // Adds a new node to a hash ring
58 | // n: name of the server
59 | // s: multiplier for default number of ticks (useful when one cache node has more resources, like RAM, than another)
60 | func (h *HashRing) AddNode(n string, s int) {
61 | tSpots := h.defaultSpots * s
62 | hash := sha1.New()
63 | for i := 1; i <= tSpots; i++ {
64 | hash.Write([]byte(n + ":" + strconv.Itoa(i)))
65 | hashBytes := hash.Sum(nil)
66 |
67 | n := &node{
68 | node: n,
69 | hash: uint(hashBytes[19]) | uint(hashBytes[18])<<8 | uint(hashBytes[17])<<16 | uint(hashBytes[16])<<24,
70 | }
71 |
72 | h.ticks = append(h.ticks, *n)
73 | hash.Reset()
74 | }
75 | }
76 |
77 | func (h *HashRing) Bake() {
78 | h.ticks.Sort()
79 | h.length = len(h.ticks)
80 | }
81 |
82 | func (h *HashRing) Hash(s string) string {
83 | hash := sha1.New()
84 | hash.Write([]byte(s))
85 | hashBytes := hash.Sum(nil)
86 | v := uint(hashBytes[19]) | uint(hashBytes[18])<<8 | uint(hashBytes[17])<<16 | uint(hashBytes[16])<<24
87 | i := sort.Search(h.length, func(i int) bool { return h.ticks[i].hash >= v })
88 |
89 | if i == h.length {
90 | i = 0
91 | }
92 |
93 | return h.ticks[i].node
94 | }
95 |
--------------------------------------------------------------------------------
/message.ini:
--------------------------------------------------------------------------------
1 | [program:gopush-message]
2 | command=/home/paas/paas/appmsgsrv/bin/message -c /home/paas/paas/appmsgsrv/bin/message.conf -log_level=info
3 | ;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
4 | numprocs=1 ; number of processes copies to start (def 1)
5 | directory=/home/paas/paas/appmsgsrv/bin ; directory to cwd to before exec (def no cwd)
6 | ;umask=022 ; umask for process (default None)
7 | ;priority=999 ; the relative start priority (default 999)
8 | autostart=true ; start at supervisord start (default: true)
9 | autorestart=true ; whether/when to restart (default: unexpected)
10 | startsecs=10 ; number of secs prog must stay running (def. 1)
11 | startretries=10 ; max # of serial start failures (default 3)
12 | ;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)
13 | ;stopsignal=QUIT ; signal used to kill process (default TERM)
14 | ;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
15 | ;stopasgroup=false ; send stop signal to the UNIX process group (default false)
16 | ;killasgroup=false ; SIGKILL the UNIX process group (def false)
17 | user=root ; setuid to this UNIX account to run the program
18 | redirect_stderr=true ; redirect proc stderr to stdout (default false)
19 | stdout_logfile=/home/paas/paas/appmsgsrv/logs/message/message.log ; stdout log path, NONE for none; default AUTO
20 | stdout_logfile_maxbytes=50MB ; max # logfile bytes b4 rotation (default 50MB)
21 | stdout_logfile_backups=10 ; # of stdout logfile backups (default 10)
22 | stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
23 | ;stdout_events_enabled=false ; emit events on stdout writes (default false)
24 | ;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO
25 | ;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
26 | ;stderr_logfile_backups=10 ; # of stderr logfile backups (default 10)
27 | ;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
28 | ;stderr_events_enabled=false ; emit events on stderr writes (default false)
29 | ;environment=A=1,B=2 ; process environment additions (def no adds)
30 | ;serverurl=AUTO ; override serverurl computation (childutils)
31 |
--------------------------------------------------------------------------------
/message/config.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "flag"
21 | "fmt"
22 | "github.com/Terry-Mao/goconf"
23 | "runtime"
24 | "time"
25 | )
26 |
27 | var (
28 | Conf *Config
29 | confFile string
30 | )
31 |
32 | func init() {
33 | flag.StringVar(&confFile, "c", "./message.conf", " set message config file path")
34 | }
35 |
36 | // Config struct
37 | type Config struct {
38 | RPCBind []string `goconf:"base:rpc.bind:,"`
39 | User string `goconf:"base:user"`
40 | PidFile string `goconf:"base:pidfile"`
41 | Dir string `goconf:"base:dir"`
42 | MaxProc int `goconf:"base:maxproc"`
43 | PprofBind []string `goconf:"base:pprof.bind:,"`
44 | StorageType string `goconf:"storage:type"`
45 | RedisIdleTimeout time.Duration `goconf:"redis:timeout:time"`
46 | RedisMaxIdle int `goconf:"redis:idle"`
47 | RedisMaxActive int `goconf:"redis:active"`
48 | RedisMaxStore int `goconf:"redis:store"`
49 | RedisKetamaBase int `goconf:"redis:ketama.base"`
50 | MySQLClean time.Duration `goconf:"mysql:clean:time"`
51 | MySQLKetamaBase int `goconf:"mysql:ketama.base"`
52 | RedisSource map[string]string `goconf:"-"`
53 | MySQLSource map[string]string `goconf:"-"`
54 | // zookeeper
55 | ZookeeperAddr []string `goconf:"zookeeper:addr:,"`
56 | ZookeeperTimeout time.Duration `goconf:"zookeeper:timeout:time"`
57 | ZookeeperPath string `goconf:"zookeeper:path"`
58 | }
59 |
60 | // NewConfig parse config file into Config.
61 | func InitConfig() error {
62 | gconf := goconf.New()
63 | if err := gconf.Parse(confFile); err != nil {
64 | logger.Errorf("goconf.Parse(\"%s\") error(%v)", confFile, err)
65 | return err
66 | }
67 | Conf = &Config{
68 | // base
69 | RPCBind: []string{"localhost:8070"},
70 | User: "nobody nobody",
71 | PidFile: "/tmp/gopush-cluster-message.pid",
72 | Dir: "./",
73 | MaxProc: runtime.NumCPU(),
74 | PprofBind: []string{"localhost:8170"},
75 | // storage
76 | StorageType: "redis",
77 | // redis
78 | RedisIdleTimeout: 28800 * time.Second,
79 | RedisMaxIdle: 50,
80 | RedisMaxActive: 1000,
81 | RedisMaxStore: 20,
82 | RedisSource: make(map[string]string),
83 | // mysql
84 | MySQLSource: make(map[string]string),
85 | MySQLClean: 1 * time.Hour,
86 | // zookeeper
87 | ZookeeperAddr: []string{"localhost:2181"},
88 | ZookeeperTimeout: 30 * time.Second,
89 | ZookeeperPath: "/gopush-cluster-message",
90 | }
91 | if err := gconf.Unmarshal(Conf); err != nil {
92 | logger.Errorf("goconf.Unmarshal() error(%v)", err)
93 | return err
94 | }
95 | // redis section
96 | redisAddrsSec := gconf.Get("redis.source")
97 | if redisAddrsSec != nil {
98 | for _, key := range redisAddrsSec.Keys() {
99 | addr, err := redisAddrsSec.String(key)
100 | if err != nil {
101 | return fmt.Errorf("config section: \"redis.addrs\" key: \"%s\" error(%v)", key, err)
102 | }
103 | Conf.RedisSource[key] = addr
104 | }
105 | }
106 | // mysql section
107 | dbSource := gconf.Get("mysql.source")
108 | if dbSource != nil {
109 | for _, key := range dbSource.Keys() {
110 | source, err := dbSource.String(key)
111 | if err != nil {
112 | return fmt.Errorf("config section: \"mysql.source\" key: \"%s\" error(%v)", key, err)
113 | }
114 | Conf.MySQLSource[key] = source
115 | }
116 | }
117 | return nil
118 | }
119 |
--------------------------------------------------------------------------------
/message/main.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "flag"
21 | "github.com/EPICPaaS/appmsgsrv/perf"
22 | "github.com/EPICPaaS/appmsgsrv/process"
23 | "github.com/EPICPaaS/appmsgsrv/ver"
24 | "github.com/b3log/wide/log"
25 | "os"
26 | "runtime"
27 | "time"
28 | )
29 |
30 | var logger = log.NewLogger(os.Stdout)
31 |
32 | func main() {
33 | logLevel := flag.String("log_level", "info", "logger level")
34 | flag.Parse()
35 | log.SetLevel(*logLevel)
36 | logger.Infof("message ver: \"%s\" start", ver.Version)
37 |
38 | if err := InitConfig(); err != nil {
39 | logger.Errorf("InitConfig() error(%v)", err)
40 | return
41 | }
42 | // Set max routine
43 | runtime.GOMAXPROCS(Conf.MaxProc)
44 | // start pprof http
45 | perf.Init(Conf.PprofBind)
46 | // Initialize redis
47 | if err := InitStorage(); err != nil {
48 | logger.Errorf("InitStorage() error(%v)", err)
49 | return
50 | }
51 | // init rpc service
52 | InitRPC()
53 | // init zookeeper
54 | zk, err := InitZK()
55 | if err != nil {
56 | logger.Errorf("InitZK() error(%v)", err)
57 | if zk != nil {
58 | zk.Close()
59 | }
60 | return
61 | }
62 | // if process exit, close zk
63 | defer zk.Close()
64 | // init process
65 | // sleep one second, let the listen start
66 | time.Sleep(time.Second)
67 | if err = process.Init(Conf.User, Conf.Dir, Conf.PidFile); err != nil {
68 | logger.Errorf("process.Init(\"%s\", \"%s\", \"%s\") error(%v)", Conf.User, Conf.Dir, Conf.PidFile, err)
69 | return
70 | }
71 | // init signals, block wait signals
72 | sig := InitSignal()
73 | HandleSignal(sig)
74 | // exit
75 | logger.Info("message stop")
76 | }
77 |
--------------------------------------------------------------------------------
/message/message-example.conf:
--------------------------------------------------------------------------------
1 | # Message configuration file example
2 |
3 | # Note on units: when memory size is needed, it is possible to specify
4 | # it in the usual form of 1k 5GB 4M and so forth:
5 | #
6 | # 1kb => 1024 bytes
7 | # 1mb => 1024*1024 bytes
8 | # 1gb => 1024*1024*1024 bytes
9 | #
10 | # units are case insensitive so 1GB 1Gb 1gB are all the same.
11 |
12 | # Note on units: when time duration is needed, it is possible to specify
13 | # it in the usual form of 1s 5M 4h and so forth:
14 | #
15 | # 1s => 1000 * 1000 * 1000 nanoseconds
16 | # 1m => 60 seconds
17 | # 1h => 60 minutes
18 | #
19 | # units are case insensitive so 1h 1H are all the same.
20 |
21 | [base]
22 | # Message service tcp listen and server on this address,
23 | # default tcp listen localhost:8070
24 | # Examples:
25 | #
26 | # rpc.bind 192.168.1.100:8070,10.0.0.1:8070
27 | # rpc.bind 127.0.0.1:8070
28 | # rpc.bind 0.0.0.0:8070
29 | rpc.bind localhost:8070
30 |
31 | # If the master process is run as root, then message will setuid()/setgid()
32 | # to USER/GROUP. If GROUP is not specified, then message uses the same name as
33 | # USER. By default it's nobody user and nobody or nogroup group.
34 | user nobody
35 |
36 | # When running daemonized, Message writes a pid file in
37 | # /tmp/gopush-cluster-message.pid by default. You can specify a custom pid file
38 | # location here.
39 | pidfile /tmp/gopush-cluster-message.pid
40 |
41 | # Sets the maximum number of CPUs that can be executing simultaneously.
42 | # This call will go away when the scheduler improves. By default the number of
43 | # logical CPUs is set.
44 | #
45 | # maxproc 4
46 |
47 | # The working directory.
48 | #
49 | # Note that you must specify a directory here, not a file name.
50 | dir ./
51 |
52 | # This is used by message service profiling (pprof).
53 | # By default message pprof listens for connections from local interfaces on 8170
54 | # port. It's not safty for listening internet IP addresses.
55 | #
56 | # Examples:
57 | #
58 | # pprof.bind 192.168.1.100:8170,10.0.0.1:8170
59 | # pprof.bind 127.0.0.1:8170
60 | # pprof.bind 0.0.0.0:8170
61 | pprof.bind localhost:8170
62 |
63 | [storage]
64 | # Storage type, Support: redis, mysql. We suggest use redis, cause mysql is low
65 | # efficency. Now, only support to run one of both in the same time
66 | type redis
67 |
68 | [redis]
69 | # Close connections after remaining idle for this duration. If the value
70 | # is zero, then idle connections are not closed. Applications should set
71 | # the timeout to a value less than the server's timeout.
72 | timeout 28800s
73 |
74 | # Maximum number of idle connections in the pool.
75 | idle 100
76 |
77 | # Maximum number of connections allocated by the pool at a given time.
78 | # When zero, there is no limit on the number of connections in the pool.
79 | active 300
80 |
81 | # Max quantity of stored message for each key, default 20
82 | store 20
83 |
84 | # ketama virtual node base number
85 | ketama.base 255
86 |
87 | [redis.source]
88 | # The format like "NodeName IP:Port", NodeName was specified by Comet service.
89 | # If there are multiple nodes, then configure following
90 | # nodeN:W, N is node name, W is node weight
91 | # node1:1 IP1:Port1
92 | # node2:2 IP2:Port2
93 | # node3:3 IP3:Port3
94 | node1:1 localhost:6379
95 |
96 | [mysql]
97 | # Delete all of expired message loop time interval
98 | clean 1h
99 |
100 | # ketama virtual node base number
101 | ketama.base 255
102 |
103 | [mysql.source]
104 | # Database source.
105 | # For example with the following
106 | # nodeN:W, N is node name, W is node weight
107 | # node1:1 test:test@(192.168.1.1:3306)/gopush?parseTime=true&loc=Local&charset=utf8
108 | # node2:1 test:test@(192.168.1.2:3306)/gopush?parseTime=true&loc=Local&charset=utf8
109 | node1:1 test:test@(192.168.1.2:3306)/gopush?parseTime=true&loc=Local&charset=utf8
110 | node2:2 test:test@(192.168.1.3:3306)/gopush?parseTime=true&loc=Local&charset=utf8
111 | node3:3 test:test@(192.168.1.4:3306)/gopush?parseTime=true&loc=Local&charset=utf8
112 |
113 | ################################## ZOOKEEPER ##################################
114 |
115 | # The zookeeper cluster section. When message start, it will register data in
116 | # the zookeeper cluster and create a ephemeral node. When message died, the
117 | # node will drop by zookeeper cluster. So we can use the feature implement node
118 | # failover.
119 | [zookeeper]
120 | # Zookeeper cluster addresses. Mutiple address split by a ",".
121 | # Examples:
122 | #
123 | # addr 192.168.1.100:2181,10.0.0.1:2181
124 | # addr 127.0.0.1:2181
125 | addr localhost:2181
126 |
127 | # Zookeeper cluster session idle timeout seconds. Zookeeper will close the
128 | # connection after a client is idle for N seconds, Zookeeper will send heartbeat
129 | # after timeout / 2 second.
130 | # Examples:
131 | #
132 | # timeout 30s
133 | # timeout 15s
134 | timeout 30s
135 |
136 | # message create ephemeral node at the specified root path.
137 | #
138 | # Note the path must start with "/".
139 | path /gopush-cluster-message
140 |
141 | ################################## INCLUDES ###################################
142 |
143 | # Include one or more other config files here. This is useful if you
144 | # have a standard template that goes to all comet server but also need
145 | # to customize a few per-server settings. Include files can include
146 | # other files, so use this wisely.
147 | #
148 | # include /path/to/local.conf
149 | # include /path/to/other.conf
150 |
--------------------------------------------------------------------------------
/message/message.conf:
--------------------------------------------------------------------------------
1 | # Message configuration file example
2 |
3 | # Note on units: when memory size is needed, it is possible to specify
4 | # it in the usual form of 1k 5GB 4M and so forth:
5 | #
6 | # 1kb => 1024 bytes
7 | # 1mb => 1024*1024 bytes
8 | # 1gb => 1024*1024*1024 bytes
9 | #
10 | # units are case insensitive so 1GB 1Gb 1gB are all the same.
11 |
12 | # Note on units: when time duration is needed, it is possible to specify
13 | # it in the usual form of 1s 5M 4h and so forth:
14 | #
15 | # 1s => 1000 * 1000 * 1000 nanoseconds
16 | # 1m => 60 seconds
17 | # 1h => 60 minutes
18 | #
19 | # units are case insensitive so 1h 1H are all the same.
20 |
21 | [base]
22 | # Message service tcp listen and server on this address,
23 | # default tcp listen 192.168.1.122:8070
24 | # Examples:
25 | #
26 | # rpc.bind 192.168.1.100:8070,10.0.0.1:8070
27 | # rpc.bind 127.0.0.1:8070
28 | # rpc.bind 0.0.0.0:8070
29 | rpc.bind 192.168.1.122:8070
30 |
31 | # If the master process is run as root, then message will setuid()/setgid()
32 | # to USER/GROUP. If GROUP is not specified, then message uses the same name as
33 | # USER. By default it's nobody user and nobody or nogroup group.
34 | user ublxd
35 |
36 | # When running daemonized, Message writes a pid file in
37 | # /tmp/gopush-cluster-message.pid by default. You can specify a custom pid file
38 | # location here.
39 | pidfile /tmp/gopush-cluster-message.pid
40 |
41 | # Sets the maximum number of CPUs that can be executing simultaneously.
42 | # This call will go away when the scheduler improves. By default the number of
43 | # logical CPUs is set.
44 | #
45 | # maxproc 4
46 |
47 | # The working directory.
48 | #
49 | # Note that you must specify a directory here, not a file name.
50 | dir ./
51 |
52 | # This is used by message service profiling (pprof).
53 | # By default message pprof listens for connections from local interfaces on 8170
54 | # port. It's not safty for listening internet IP addresses.
55 | #
56 | # Examples:
57 | #
58 | # pprof.bind 192.168.1.100:8170,10.0.0.1:8170
59 | # pprof.bind 127.0.0.1:8170
60 | # pprof.bind 0.0.0.0:8170
61 | pprof.bind 192.168.1.122:8170
62 |
63 | [storage]
64 | # Storage type, Support: redis, mysql. We suggest use redis, cause mysql is low
65 | # efficency. Now, only support to run one of both in the same time
66 | type mysql
67 |
68 | [redis]
69 | # Close connections after remaining idle for this duration. If the value
70 | # is zero, then idle connections are not closed. Applications should set
71 | # the timeout to a value less than the server's timeout.
72 | timeout 28800s
73 |
74 | # Maximum number of idle connections in the pool.
75 | idle 100
76 |
77 | # Maximum number of connections allocated by the pool at a given time.
78 | # When zero, there is no limit on the number of connections in the pool.
79 | active 300
80 |
81 | # Max quantity of stored message for each key, default 20
82 | store 20
83 |
84 | # ketama virtual node base number
85 | ketama.base 255
86 |
87 | [redis.source]
88 | # The format like "NodeName IP:Port", NodeName was specified by Comet service.
89 | # If there are multiple nodes, then configure following
90 | # nodeN:W, N is node name, W is node weight
91 | # node1:1 IP1:Port1
92 | # node2:2 IP2:Port2
93 | # node3:3 IP3:Port3
94 | node1:1 192.168.1.122:6379
95 |
96 | [mysql]
97 | # Delete all of expired message loop time interval
98 | clean 1h
99 |
100 | # ketama virtual node base number
101 | ketama.base 255
102 |
103 | [mysql.source]
104 | # Database source.
105 | # For example with the following
106 | # nodeN:W, N is node name, W is node weight
107 | # node1:1 test:test@(192.168.1.1:3306)/gopush?parseTime=true&loc=Local&charset=utf8
108 | # node2:1 test:test@(192.168.1.2:3306)/gopush?parseTime=true&loc=Local&charset=utf8
109 | node1:1 root:123456@(10.180.120.63:3308)/appmsgsrv?parseTime=true&loc=Local&charset=utf8
110 | #node2:2 test:test@(192.168.1.3:3306)/gopush?parseTime=true&loc=Local&charset=utf8
111 | #node3:3 test:test@(192.168.1.4:3306)/gopush?parseTime=true&loc=Local&charset=utf8
112 |
113 | ################################## ZOOKEEPER ##################################
114 |
115 | # The zookeeper cluster section. When message start, it will register data in
116 | # the zookeeper cluster and create a ephemeral node. When message died, the
117 | # node will drop by zookeeper cluster. So we can use the feature implement node
118 | # failover.
119 | [zookeeper]
120 | # Zookeeper cluster addresses. Mutiple address split by a ",".
121 | # Examples:
122 | #
123 | # addr 192.168.1.100:2181,10.0.0.1:2181
124 | # addr 127.0.0.1:2181
125 | addr 192.168.1.122:2181
126 |
127 | # Zookeeper cluster session idle timeout seconds. Zookeeper will close the
128 | # connection after a client is idle for N seconds, Zookeeper will send heartbeat
129 | # after timeout / 2 second.
130 | # Examples:
131 | #
132 | # timeout 30s
133 | # timeout 15s
134 | timeout 30s
135 |
136 | # message create ephemeral node at the specified root path.
137 | #
138 | # Note the path must start with "/".
139 | path /gopush-cluster-message
140 |
141 | ################################## INCLUDES ###################################
142 |
143 | # Include one or more other config files here. This is useful if you
144 | # have a standard template that goes to all comet server but also need
145 | # to customize a few per-server settings. Include files can include
146 | # other files, so use this wisely.
147 | #
148 | # include /path/to/local.conf
149 | # include /path/to/other.conf
150 |
--------------------------------------------------------------------------------
/message/rpc.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | myrpc "github.com/EPICPaaS/appmsgsrv/rpc"
21 | "net"
22 | "net/rpc"
23 | )
24 |
25 | // RPC For receive offline messages
26 | type MessageRPC struct {
27 | }
28 |
29 | // InitRPC start accept rpc call.
30 | func InitRPC() {
31 | msg := &MessageRPC{}
32 | rpc.Register(msg)
33 | for _, bind := range Conf.RPCBind {
34 | logger.Infof("start rpc listen addr: \"%s\"", bind)
35 | go rpcListen(bind)
36 | }
37 | }
38 |
39 | func rpcListen(bind string) {
40 | l, err := net.Listen("tcp", bind)
41 | if err != nil {
42 | logger.Errorf("net.Listen(\"tcp\", \"%s\") error(%v)", bind, err)
43 | panic(err)
44 | }
45 | defer func() {
46 | if err := l.Close(); err != nil {
47 | logger.Errorf("listener.Close() error(%v)", err)
48 | }
49 | }()
50 | rpc.Accept(l)
51 | }
52 |
53 | // SavePrivate rpc interface save user private message.
54 | func (r *MessageRPC) SavePrivate(m *myrpc.MessageSavePrivateArgs, ret *int) error {
55 | if m == nil || m.Msg == nil || m.MsgId < 0 {
56 | return myrpc.ErrParam
57 | }
58 | if err := UseStorage.SavePrivate(m.Key, m.Msg, m.MsgId, m.Expire); err != nil {
59 | logger.Errorf("UseStorage.SavePrivate(\"%s\", \"%s\", %d, %d) error(%v)", m.Key, string(m.Msg), m.MsgId, m.Expire, err)
60 | return err
61 | }
62 | logger.Tracef("UseStorage.SavePrivate(\"%s\", \"%s\", %d, %d) ok", m.Key, string(m.Msg), m.MsgId, m.Expire)
63 | return nil
64 | }
65 |
66 | // GetPrivate rpc interface get user private message.
67 | func (r *MessageRPC) GetPrivate(m *myrpc.MessageGetPrivateArgs, rw *myrpc.MessageGetResp) error {
68 | logger.Tracef("messageRPC.GetPrivate key:\"%s\" mid:\"%d\"", m.Key, m.MsgId)
69 | if m == nil || m.Key == "" || m.MsgId < 0 {
70 | return myrpc.ErrParam
71 | }
72 | msgs, err := UseStorage.GetPrivate(m.Key, m.MsgId)
73 | if err != nil {
74 | logger.Errorf("UseStorage.GetPrivate(\"%s\", %d) error(%v)", m.Key, m.MsgId, err)
75 | return err
76 | }
77 | rw.Msgs = msgs
78 | logger.Tracef("UserStorage.GetPrivate(\"%s\", %d) ok", m.Key, m.MsgId)
79 | return nil
80 | }
81 |
82 | // DelPrivate rpc interface delete user private message.
83 | func (r *MessageRPC) DelPrivate(key string, ret *int) error {
84 | if key == "" {
85 | return myrpc.ErrParam
86 | }
87 | if err := UseStorage.DelPrivate(key); err != nil {
88 | logger.Errorf("UserStorage.DelPrivate(\"%s\") error(%v)", key, err)
89 | return err
90 | }
91 | logger.Tracef("UserStorage.DelPrivate(\"%s\") ok", key)
92 | return nil
93 | }
94 |
95 | /*
96 | // SavePublish rpc interface save publish message.
97 | func (r *MessageRPC) SavePublish(m *myrpc.MessageSaveGroupArgs, ret *int) error {
98 | return nil
99 | }
100 |
101 | // GetPublish rpc interface get publish message.
102 | func (r *MessageRPC) GetPublish(m *myrpc.MessageGetGroupArgs, rw *myrpc.MessageGetResp) error {
103 | return nil
104 | }
105 |
106 | // DelPublish rpc interface delete publish message.
107 | func (r *MessageRPC) DelPublish(key string, ret *int) error {
108 | return nil
109 | }
110 |
111 | // SaveGroup rpc interface save publish message.
112 | func (r *MessageRPC) SaveGroup(m *myrpc.MessageSaveGroupArgs, ret *int) error {
113 | return nil
114 | }
115 |
116 | // GetPublish rpc interface get publish message.
117 | func (r *MessageRPC) GetGroup(m *myrpc.MessageGetGroupArgs, rw *myrpc.MessageGetResp) error {
118 | return nil
119 | }
120 |
121 | // DelPublish rpc interface delete publish message.
122 | func (r *MessageRPC) DelGroup(key string, ret *int) error {
123 | return nil
124 | }
125 | */
126 |
127 | // Server Ping interface
128 | func (r *MessageRPC) Ping(p int, ret *int) error {
129 | logger.Trace("ping ok")
130 | return nil
131 | }
132 |
--------------------------------------------------------------------------------
/message/signal.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "os"
21 | "os/signal"
22 | "syscall"
23 | )
24 |
25 | // InitSignal register signals handler.
26 | func InitSignal() chan os.Signal {
27 | c := make(chan os.Signal, 1)
28 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGSTOP)
29 | return c
30 | }
31 |
32 | // HandleSignal fetch signal from chan then do exit or reload.
33 | func HandleSignal(c chan os.Signal) {
34 | // Block until a signal is received.
35 | for {
36 | s := <-c
37 | logger.Infof("get a signal %s", s.String())
38 | switch s {
39 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT:
40 | logger.Error("Comet Exit: syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT")
41 | return
42 | case syscall.SIGHUP:
43 | // TODO reload
44 | //return
45 | logger.Error("Comet Exit: syscall.SIGHUP")
46 | default:
47 | logger.Error("Comet Exit: default")
48 | return
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/message/storage.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "encoding/json"
21 | "errors"
22 | "github.com/EPICPaaS/appmsgsrv/rpc"
23 | )
24 |
25 | const (
26 | RedisStorageType = "redis"
27 | MySQLStorageType = "mysql"
28 | )
29 |
30 | var (
31 | UseStorage Storage
32 | ErrStorageType = errors.New("unknown storage type")
33 | )
34 |
35 | // Stored messages interface
36 | type Storage interface {
37 | // private message method
38 | GetPrivate(key string, mid int64) ([]*rpc.Message, error)
39 | SavePrivate(key string, msg json.RawMessage, mid int64, expire uint) error
40 | DelPrivate(key string) error
41 | }
42 |
43 | // InitStorage init the storage type(mysql or redis).
44 | func InitStorage() error {
45 | if Conf.StorageType == RedisStorageType {
46 | UseStorage = NewRedisStorage()
47 | } else if Conf.StorageType == MySQLStorageType {
48 | UseStorage = NewMySQLStorage()
49 | } else {
50 | logger.Errorf("unknown storage type: \"%s\"", Conf.StorageType)
51 | return ErrStorageType
52 | }
53 | return nil
54 | }
55 |
--------------------------------------------------------------------------------
/message/zk.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | // github.com/samuel/go-zookeeper
18 | // Copyright (c) 2013, Samuel Stauffer
19 | // All rights reserved.
20 |
21 | package main
22 |
23 | import (
24 | myzk "github.com/EPICPaaS/appmsgsrv/zk"
25 | "github.com/samuel/go-zookeeper/zk"
26 | "strings"
27 | )
28 |
29 | // InitZK create zookeeper root path, and register a temp node.
30 | func InitZK() (*zk.Conn, error) {
31 | conn, err := myzk.Connect(Conf.ZookeeperAddr, Conf.ZookeeperTimeout)
32 | if err != nil {
33 | logger.Errorf("zk.Connect() error(%v)", err)
34 | return nil, err
35 | }
36 | if err = myzk.Create(conn, Conf.ZookeeperPath); err != nil {
37 | logger.Errorf("zk.Create() error(%v)", err)
38 | return conn, err
39 | }
40 | data := strings.Join(Conf.RPCBind, ",")
41 | logger.Tracef("zk data: \"%s\"", data)
42 | // tcp, websocket and rpc bind address store in the zk
43 | if err = myzk.RegisterTemp(conn, Conf.ZookeeperPath, data); err != nil {
44 | logger.Errorf("zk.RegisterTemp() error(%v)", err)
45 | return conn, err
46 | }
47 | return conn, nil
48 | }
49 |
--------------------------------------------------------------------------------
/perf/perf.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package perf
18 |
19 | import (
20 | "github.com/b3log/wide/log"
21 | "net/http"
22 | "net/http/pprof"
23 | "os"
24 | )
25 |
26 | var logger = log.NewLogger(os.Stdout)
27 |
28 | // StartPprof start http pprof.
29 | func Init(pprofBind []string) {
30 | pprofServeMux := http.NewServeMux()
31 | pprofServeMux.HandleFunc("/debug/pprof/", pprof.Index)
32 | pprofServeMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
33 | pprofServeMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
34 | pprofServeMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
35 | for _, addr := range pprofBind {
36 | go func() {
37 | if err := http.ListenAndServe(addr, pprofServeMux); err != nil {
38 | logger.Errorf("http.ListenAndServe(\"%s\", pprofServeMux) error(%v)", addr, err)
39 | panic(err)
40 | }
41 | }()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/process/process.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package process
18 |
19 | import (
20 | "fmt"
21 | "io/ioutil"
22 | "os"
23 | //"os/user"
24 | //"strconv"
25 | //"strings"
26 | //"syscall"
27 | )
28 |
29 | const (
30 | defaultUser = "nobody"
31 | defaultGroup = "nobody"
32 | )
33 |
34 | // Init create pid file, set working dir, setgid and setuid.
35 | func Init(userGroup, dir, pidFile string) error {
36 | // change working dir
37 | if err := os.Chdir(dir); err != nil {
38 | return err
39 | }
40 | // create pid file
41 | if err := ioutil.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644); err != nil {
42 | return err
43 | }
44 | /*
45 | // setuid and setgid
46 | ug := strings.SplitN(userGroup, " ", 2)
47 | usr := defaultUser
48 | grp := defaultGroup
49 | if len(ug) == 0 {
50 | // default user and group (nobody)
51 | } else if len(ug) == 1 {
52 | usr = ug[0]
53 | grp = ""
54 | } else if len(ug) == 2 {
55 | usr = ug[0]
56 | grp = ug[1]
57 | }
58 | uid := 0
59 | gid := 0
60 | ui, err := user.Lookup(usr)
61 | if err != nil {
62 | return err
63 | }
64 | uid, _ = strconv.Atoi(ui.Uid)
65 | // group no set
66 | if grp == "" {
67 | gid, _ = strconv.Atoi(ui.Gid)
68 | } else {
69 | // use user's group instread
70 | // TODO LookupGroup
71 | gid, _ = strconv.Atoi(ui.Gid)
72 | }
73 | if err := syscall.Setgid(gid); err != nil {
74 | fmt.Println(1)
75 | return err
76 | }
77 | if err := syscall.Setuid(uid); err != nil {
78 | fmt.Println(2)
79 | return err
80 | }*/
81 | return nil
82 | }
83 |
--------------------------------------------------------------------------------
/process/process_test.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package process
18 |
19 | import (
20 | "testing"
21 | )
22 |
23 | // mkdir -p /tmp/test && chown -R nobody:nobody /tmp/test
24 | // sudo go test
25 | func TestInit(t *testing.T) {
26 | if err := Init("nobody nobody", "./", "/tmp/test/process_test.pid"); err != nil {
27 | t.Error(err)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/router/cn.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package router
18 |
19 | import (
20 | "fmt"
21 | "github.com/thinkboy/go-qqwry"
22 | "strings"
23 | )
24 |
25 | const (
26 | CTCC = "电信"
27 | CMCC = "移动"
28 | CUCC = "联通"
29 | ENET = "教育网"
30 | )
31 |
32 | type RouterCN struct {
33 | qqWryReader *qqwry.QQWry
34 | }
35 |
36 | // Init china network router
37 | func InitCN(path string) (*RouterCN, error) {
38 | qqWryRead, err := qqwry.NewQQWry(path)
39 | if err != nil {
40 | return nil, fmt.Errorf("load QQWry.dat error : %v", err)
41 | }
42 |
43 | return &RouterCN{qqWryReader: qqWryRead}, nil
44 | }
45 |
46 | // Select the best ip
47 | func (r *RouterCN) SelectBest(remoteAddr string, ips []string) string {
48 | if len(ips) == 0 {
49 | return ""
50 | }
51 |
52 | ipRemote := strings.Split(remoteAddr, ":")
53 | cc := r.netName(ipRemote[0])
54 | if cc == "" {
55 | return ""
56 | }
57 |
58 | for i := 0; i < len(ips); i++ {
59 | ip := strings.Split(ips[i], ":")
60 | if cc == r.netName(ip[0]) {
61 | return ips[i]
62 | }
63 | }
64 |
65 | return ""
66 | }
67 |
68 | func (r *RouterCN) netName(ip string) string {
69 | cc := ""
70 | _, area := r.qqWryReader.QueryIP(ip)
71 | if -1 != strings.Index(area, CTCC) {
72 | cc = CTCC
73 | } else if -1 != strings.Index(area, CMCC) {
74 | cc = CMCC
75 | } else if -1 != strings.Index(area, CUCC) {
76 | cc = CUCC
77 | } else if -1 != strings.Index(area, ENET) {
78 | cc = ENET
79 | }
80 |
81 | return cc
82 | }
83 |
--------------------------------------------------------------------------------
/rpc/rand_lb.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "errors"
21 | "fmt"
22 | "math/rand"
23 | "net/rpc"
24 | "time"
25 | )
26 |
27 | const (
28 | randLBRetryCHLength = 10
29 | )
30 |
31 | var (
32 | ErrRandLBLength = errors.New("clients and addrs length not match")
33 | ErrRandLBAddr = errors.New("clients map no addr key")
34 | )
35 |
36 | // random load balancing object
37 | type RandLB struct {
38 | Clients map[string]*rpc.Client
39 | addrs []string
40 | length int
41 | exitCH chan int
42 | }
43 |
44 | // NewRandLB new a random load balancing object.
45 | func NewRandLB(clients map[string]*rpc.Client, addrs []string, service string, retry, ping time.Duration, check bool) (*RandLB, error) {
46 | length := len(clients)
47 | if length != len(addrs) {
48 | logger.Errorf("clients: %d, addrs: %d not equal", length, len(addrs))
49 | return nil, ErrRandLBLength
50 | }
51 | for _, addr := range addrs {
52 | if _, ok := clients[addr]; !ok {
53 | logger.Errorf("addr: \"%s\" not exist in clients map", addr)
54 | return nil, ErrRandLBAddr
55 | }
56 | }
57 | r := &RandLB{Clients: clients, addrs: addrs, length: length}
58 | if check && length > 0 {
59 | logger.Info("rpc ping start")
60 | r.ping(service, retry, ping)
61 | }
62 | return r, nil
63 | }
64 |
65 | // Get get a rpc client randomly.
66 | func (r *RandLB) Get() *rpc.Client {
67 | if len(r.addrs) == 0 {
68 | return nil
69 | }
70 | addr := r.addrs[rand.Intn(r.length)]
71 | logger.Tracef("rand hit rpc node: \"%s\"", addr)
72 | client, _ := r.Clients[addr]
73 | return client
74 | }
75 |
76 | // Stop stop the retry connect goroutine and ping goroutines.
77 | func (r *RandLB) Stop() {
78 | if r.exitCH != nil {
79 | close(r.exitCH)
80 | }
81 | logger.Info("stop the randlb retry connect goroutine and ping goroutines")
82 | }
83 |
84 | // Destroy release the rpc.Client resource.
85 | func (r *RandLB) Destroy() {
86 | r.Stop()
87 | for _, client := range r.Clients {
88 | if client != nil {
89 | if err := client.Close(); err != nil {
90 | logger.Errorf("client.Close() error(%v)", err)
91 | }
92 | }
93 | }
94 | }
95 |
96 | // ping do a ping, if failed then retry.
97 | func (r *RandLB) ping(service string, retry, ping time.Duration) {
98 | method := fmt.Sprintf("%s.Ping", service)
99 | retryCH := make(chan string, randLBRetryCHLength)
100 | r.exitCH = make(chan int, 1)
101 | for _, addr := range r.addrs {
102 | // warn: closures problem
103 | go func(addr string) {
104 | logger.Infof("\"%s\" rpc ping goroutine start", addr)
105 | ret := 0
106 | for {
107 | select {
108 | case <-r.exitCH:
109 | logger.Infof("\"%s\" rpc ping goroutine exit", addr)
110 | return
111 | default:
112 | }
113 | // get client for ping
114 | client, _ := r.Clients[addr]
115 | if err := client.Call(method, 0, &ret); err != nil {
116 | // if failed send to chan reconnect, sleep
117 | client.Close()
118 | retryCH <- addr
119 | logger.Errorf("client.Call(\"%s\", 0, &ret) error(%v), retry", method, err)
120 | time.Sleep(retry)
121 | continue
122 | }
123 | // if ok, sleep
124 | logger.Tracef("\"%s\": rpc ping ok", addr)
125 | time.Sleep(ping)
126 | }
127 | }(addr)
128 | }
129 | // rpc retry connect
130 | go func() {
131 | var retryAddr string
132 | logger.Info("rpc retry connect goroutine start")
133 | for {
134 | select {
135 | case retryAddr = <-retryCH:
136 | case <-r.exitCH:
137 | logger.Info("rpc retry connect goroutine exit")
138 | return
139 | }
140 | rpcTmp, err := rpc.Dial("tcp", retryAddr)
141 | if err != nil {
142 | logger.Errorf("rpc.Dial(\"tcp\", %s) error(%s)", retryAddr, err)
143 | continue
144 | }
145 | logger.Infof("rpc.Dial(\"tcp\", %s) retry succeed", retryAddr)
146 | // copy-on-write
147 | tmpClients := make(map[string]*rpc.Client, r.length)
148 | for addr, client := range r.Clients {
149 | tmpClients[addr] = client
150 | }
151 | tmpClients[retryAddr] = rpcTmp
152 | // atomic update clients
153 | r.Clients = tmpClients
154 | }
155 | }()
156 | }
157 |
--------------------------------------------------------------------------------
/rpc/ret.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "errors"
21 | )
22 |
23 | const (
24 | // common
25 | // ok
26 | OK = 0
27 | // param error
28 | ParamErr = 65534
29 | // internal error
30 | InternalErr = 65535
31 | )
32 |
33 | var (
34 | ErrParam = errors.New("parameter error")
35 | )
36 |
--------------------------------------------------------------------------------
/rpc/zk.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package rpc
18 |
19 | import (
20 | "time"
21 | )
22 |
23 | const (
24 | // node event
25 | eventNodeAdd = 1
26 | eventNodeDel = 2
27 | eventNodeUpdate = 3
28 |
29 | // wait node
30 | waitNodeDelay = 3
31 | waitNodeDelaySecond = waitNodeDelay * time.Second
32 | )
33 |
--------------------------------------------------------------------------------
/sql/gopush-cluster.sql:
--------------------------------------------------------------------------------
1 | # version 1.0
2 | # author Terry.Mao
3 | CREATE DATABASE IF NOT EXISTS gopush;
4 | USE gopush;
5 |
6 | # private message
7 | # DROP TABLE private_msg;
8 | CREATE TABLE IF NOT EXISTS private_msg (
9 | id bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, # auto increment id
10 | skey varchar(64) NOT NULL, # subscriber key
11 | mid bigint unsigned NOT NULL, # message id
12 | ttl bigint NOT NULL, # message expire second
13 | msg blob NOT NULL, # message content
14 | ctime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # create time
15 | mtime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # modify time
16 | UNIQUE KEY ux_private_msg_1 (skey, mid),
17 | INDEX ix_private_msg_1 (ttl)
18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
19 |
20 | # public message
21 | # DROP TABLE public_msg;
22 | CREATE TABLE IF NOT EXISTS public_msg (
23 | id bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, # auto increment id
24 | mid bigint unsigned NOT NULL, # message id
25 | ttl bigint NOT NULL, # message expire second
26 | msg blob NOT NULL, # message content
27 | ctime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # create time
28 | mtime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # modify time
29 | INDEX ix_public_msg_1 (ttl)
30 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
31 |
32 | CREATE TABLE IF NOT EXISTS public_msg_log (
33 | mid bigint unsigned NOT NULL, # message id
34 | stime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # create time
35 | ftime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # modify time
36 | UNIQUE KEY ux_public_msg_log_1 (mid)
37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
38 |
39 | # group message
40 | # DROP TABLE group_msg;
41 | CREATE TABLE IF NOT EXISTS group_msg (
42 | id bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, # auto increment id
43 | gid int unsigned NOT NULL, # group id
44 | mid bigint unsigned NOT NULL, # message id
45 | ttl bigint NOT NULL, # message expire second
46 | msg blob NOT NULL, # message content
47 | ctime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # create time
48 | mtime timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', # modify time
49 | UNIQUE KEY ux_group_msg_1 (gid, mid),
50 | INDEX ix_group_msg_1 (ttl)
51 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--------------------------------------------------------------------------------
/start_all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cdir=`dirname $0`
4 | cd $cdir
5 |
6 | nohup ./web -v=3 -log_dir="../logs/web/" -stderrthreshold=INFO &
7 | nohup ./message -v=1 -log_dir="../logs/message/" -stderrthreshold=FATAL &
8 | nohup ./comet -v=1 -log_dir="../logs/comet/" -stderrthreshold=FATAL &
9 |
10 | cd -
11 |
--------------------------------------------------------------------------------
/stop_all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cdir=`dirname $0`
4 | cd $cdir
5 |
6 | kill `cat /tmp/gopush-cluster-comet.pid`
7 | kill `cat /tmp/gopush-cluster-message.pid`
8 | kill `cat /tmp/gopush-cluster-web.pid`
9 |
10 | cd -
11 |
--------------------------------------------------------------------------------
/test/comet/comet-test-exmaple.conf:
--------------------------------------------------------------------------------
1 | # Comet test configuration file example
2 |
3 | # Note on units: when memory size is needed, it is possible to specify
4 | # it in the usual form of 1k 5GB 4M and so forth:
5 | #
6 | # 1kb => 1024 bytes
7 | # 1mb => 1024*1024 bytes
8 | # 1gb => 1024*1024*1024 bytes
9 | #
10 | # units are case insensitive so 1GB 1Gb 1gB are all the same.
11 |
12 | # Note on units: when time duration is needed, it is possible to specify
13 | # it in the usual form of 1s 5M 4h and so forth:
14 | #
15 | # 1s => 1000 * 1000 * 1000 nanoseconds
16 | # 1m => 60 seconds
17 | # 1h => 60 minutes
18 | #
19 | # units are case insensitive so 1h 1H are all the same.
20 |
21 | [base]
22 | # Set connect comet address.
23 | #
24 | # Note this directive is only support "tcp" protocol
25 | addr 42.96.176.106:6969
26 |
27 | # Set test subscriber key
28 | key Terry-Mao
29 |
30 | # Set test comet heartbeat second.
31 | heartbeat 30
32 |
33 | ################################## INCLUDES ###################################
34 |
35 | # Include one or more other config files here. This is useful if you
36 | # have a standard template that goes to all comet server but also need
37 | # to customize a few per-server settings. Include files can include
38 | # other files, so use this wisely.
39 | #
40 | # include /path/to/local.conf
41 | # include /path/to/other.conf
42 |
--------------------------------------------------------------------------------
/test/comet/config.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "errors"
21 | "flag"
22 | "github.com/Terry-Mao/goconf"
23 | "github.com/golang/glog"
24 | )
25 |
26 | var (
27 | Conf *Config
28 | ConfFile string
29 | ErrNoConfigSection = errors.New("no config section")
30 | )
31 |
32 | func init() {
33 | flag.StringVar(&ConfFile, "c", "./comet-test.conf", " set gopush-cluster comet test config file path")
34 | }
35 |
36 | type Config struct {
37 | // base
38 | Addr string `goconf:"base:addr"`
39 | Key string `goconf:"base:key"`
40 | Heartbeat int `goconf:"base:heartbeat"`
41 | }
42 |
43 | // InitConfig get a new Config struct.
44 | func InitConfig(file string) (*Config, error) {
45 | cf := &Config{
46 | // base
47 | Addr: "localhost:6969",
48 | Key: "EPICPaaS",
49 | Heartbeat: 30,
50 | }
51 | c := goconf.New()
52 | if err := c.Parse(file); err != nil {
53 | glog.Errorf("goconf.Parse(\"%s\") failed (%s)", file, err.Error())
54 | return nil, err
55 | }
56 | if err := c.Unmarshal(cf); err != nil {
57 | glog.Errorf("goconf.Unmarshal() failed (%s)", err.Error())
58 | return nil, err
59 | }
60 | return cf, nil
61 | }
62 |
--------------------------------------------------------------------------------
/test/comet/main.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "bufio"
21 | "flag"
22 | "fmt"
23 | "github.com/golang/glog"
24 | "net"
25 | "strconv"
26 | "time"
27 | )
28 |
29 | func main() {
30 | var err error
31 | flag.Parse()
32 | defer glog.Flush()
33 | // init config
34 | Conf, err = InitConfig(ConfFile)
35 | if err != nil {
36 | glog.Errorf("NewConfig(\"%s\") failed (%s)", ConfFile, err.Error())
37 | return
38 | }
39 | addr, err := net.ResolveTCPAddr("tcp", Conf.Addr)
40 | if err != nil {
41 | glog.Errorf("net.ResolveTCPAddr(\"tcp\", \"%s\") failed (%s)", Conf.Addr, err.Error())
42 | return
43 | }
44 | glog.Info("connect to gopush-cluster comet")
45 | conn, err := net.DialTCP("tcp", nil, addr)
46 | if err != nil {
47 | glog.Errorf("net.DialTCP() failed (%s)", err.Error())
48 | return
49 | }
50 | glog.Infof("send sub request")
51 | proto := []byte(fmt.Sprintf("*3\r\n$3\r\nsub\r\n$%d\r\n%s\r\n$%d\r\n%d\r\n", len(Conf.Key), Conf.Key, len(strconv.Itoa(int(Conf.Heartbeat))), Conf.Heartbeat))
52 | glog.Infof("send protocol: %s", string(proto))
53 | if _, err := conn.Write(proto); err != nil {
54 | glog.Errorf("conn.Write() failed (%s)", err.Error())
55 | return
56 | }
57 | // get first heartbeat
58 | first := false
59 | rd := bufio.NewReader(conn)
60 | // block read reply from service
61 | glog.Info("wait message")
62 | for {
63 | if err := conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(Conf.Heartbeat) * 2)); err != nil {
64 | glog.Errorf("conn.SetReadDeadline() failed (%s)", err.Error())
65 | return
66 | }
67 | line, err := rd.ReadBytes('\n')
68 | if err != nil {
69 | glog.Errorf("rd.ReadBytes() failed (%s)", err.Error())
70 | return
71 | }
72 | if line[len(line)-2] != '\r' {
73 | glog.Errorf("protocol reply format error")
74 | return
75 | }
76 | glog.Infof("line: %s", line)
77 | switch line[0] {
78 | // reply
79 | case '$':
80 | cmdSize, err := strconv.Atoi(string(line[1 : len(line)-2]))
81 | if err != nil {
82 | glog.Errorf("protocol reply format error")
83 | return
84 | }
85 | data, err := rd.ReadBytes('\n')
86 | if err != nil {
87 | glog.Errorf("protocol reply format error")
88 | return
89 | }
90 | if len(data) != cmdSize+2 {
91 | glog.Errorf("protocol reply format error: %s", data)
92 | return
93 | }
94 | if data[cmdSize] != '\r' || data[cmdSize+1] != '\n' {
95 | glog.Errorf("protocol reply format error")
96 | return
97 | }
98 | reply := string(data[0:cmdSize])
99 | glog.Infof("receive msg: %s", reply)
100 | break
101 | // heartbeat
102 | case '+':
103 | if !first {
104 | // send heartbeat
105 | go func() {
106 | for {
107 | glog.Info("send heartbeat")
108 | if _, err := conn.Write([]byte("h")); err != nil {
109 | glog.Errorf("conn.Write() failed (%s)", err.Error())
110 | return
111 | }
112 | time.Sleep(time.Duration(Conf.Heartbeat) * time.Second)
113 | }
114 | }()
115 | first = true
116 | }
117 | glog.Info("receive heartbeat")
118 | break
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/test/load/tcpclient.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 | //"strconv"
8 | "sync"
9 | "time"
10 | )
11 |
12 | func tcpClient() {
13 |
14 | tcpAddr, err := net.ResolveTCPAddr("tcp", "115.29.226.14:6969")
15 | checkError(err)
16 | conn, err := net.DialTCP("tcp", nil, tcpAddr)
17 | checkError(err)
18 | conn.SetKeepAlive(true)
19 | //链接添加成功 {userI_id}_{device_type}-{real_device_id}@{xx}
20 | key := "23370005043469383_Android-123@user"
21 | proto := []byte(fmt.Sprintf("*3\r\n$3\r\nsub\r\n$%d\r\n%s\r\n$2\r\n30\r\n", len(key), key))
22 | var w sync.WaitGroup
23 | w.Add(1)
24 | go readMsg(conn, w)
25 | if i, err := conn.Write(proto); err != nil {
26 | fmt.Println(err)
27 | } else {
28 | fmt.Println("成功:", i)
29 | }
30 | ht := time.NewTicker(10 * time.Second)
31 |
32 | for _ = range ht.C {
33 | if _, err := conn.Write([]byte("h")); err != nil {
34 | w.Done()
35 | fmt.Println("发送信息失败:", err)
36 | }
37 | }
38 | w.Wait()
39 | fmt.Println("结束")
40 | }
41 | func readMsg(conn net.Conn, w sync.WaitGroup) {
42 | msg := make([]byte, 512)
43 |
44 | for {
45 | n, err := conn.Read(msg)
46 | if err == nil {
47 | fmt.Println("读取到消息:", string(msg[:n]))
48 | } else {
49 | fmt.Println("读取:", err)
50 | w.Done()
51 | break
52 | }
53 | }
54 | }
55 | func checkError(err error) {
56 | if err != nil {
57 | fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ver/ver.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package ver
18 |
19 | const (
20 | Version = "1.0.4"
21 | )
22 |
--------------------------------------------------------------------------------
/web.ini:
--------------------------------------------------------------------------------
1 | [program:gopush-web]
2 | command=/home/paas/paas/appmsgsrv/bin/web -c /home/paas/paas/appmsgsrv/bin/web.conf -dbc /home/paas/paas/appmsgsrv/bin/db.conf -log_level=info
3 | ;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
4 | numprocs=1 ; number of processes copies to start (def 1)
5 | directory=/home/paas/paas/appmsgsrv/bin ; directory to cwd to before exec (def no cwd)
6 | ;umask=022 ; umask for process (default None)
7 | ;priority=999 ; the relative start priority (default 999)
8 | autostart=true ; start at supervisord start (default: true)
9 | autorestart=true ; whether/when to restart (default: unexpected)
10 | startsecs=10 ; number of secs prog must stay running (def. 1)
11 | startretries=10 ; max # of serial start failures (default 3)
12 | ;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2)
13 | ;stopsignal=QUIT ; signal used to kill process (default TERM)
14 | ;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
15 | ;stopasgroup=false ; send stop signal to the UNIX process group (default false)
16 | ;killasgroup=false ; SIGKILL the UNIX process group (def false)
17 | user=root ; setuid to this UNIX account to run the program
18 | redirect_stderr=true ; redirect proc stderr to stdout (default false)
19 | stdout_logfile=/home/paas/paas/appmsgsrv/logs/web/web.log ; stdout log path, NONE for none; default AUTO
20 | stdout_logfile_maxbytes=50MB ; max # logfile bytes b4 rotation (default 50MB)
21 | stdout_logfile_backups=10 ; # of stdout logfile backups (default 10)
22 | stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
23 | ;stdout_events_enabled=false ; emit events on stdout writes (default false)
24 | ;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO
25 | ;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
26 | ;stderr_logfile_backups=10 ; # of stderr logfile backups (default 10)
27 | ;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
28 | ;stderr_events_enabled=false ; emit events on stderr writes (default false)
29 | ;environment=A=1,B=2 ; process environment additions (def no adds)
30 | ;serverurl=AUTO ; override serverurl computation (childutils)
31 |
--------------------------------------------------------------------------------
/web/admin.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "encoding/json"
21 | "github.com/EPICPaaS/appmsgsrv/app"
22 | myrpc "github.com/EPICPaaS/appmsgsrv/rpc"
23 | "io/ioutil"
24 | "net/http"
25 | "net/url"
26 | "strconv"
27 | "time"
28 | )
29 |
30 | // PushPrivate handle for push private message.
31 | func PushPrivate(w http.ResponseWriter, r *http.Request) {
32 | if r.Method != "POST" {
33 | http.Error(w, "Method Not Allowed", 405)
34 | return
35 | }
36 | body := ""
37 |
38 | res := map[string]interface{}{"ret": app.OK}
39 |
40 | defer app.RetPWrite(w, r, res, &body, time.Now())
41 | // param
42 | bodyBytes, err := ioutil.ReadAll(r.Body)
43 | if err != nil {
44 | res["ret"] = app.ParamErr
45 | logger.Errorf("ioutil.ReadAll() failed (%s)", err.Error())
46 | return
47 | }
48 | body = string(bodyBytes)
49 | params := r.URL.Query()
50 | key := params.Get("key")
51 | expireStr := params.Get("expire")
52 | if key == "" {
53 | res["ret"] = app.ParamErr
54 | return
55 | }
56 | expire, err := strconv.ParseUint(expireStr, 10, 32)
57 | if err != nil {
58 | res["ret"] = app.ParamErr
59 | logger.Errorf("strconv.ParseUint(\"%s\", 10, 32) error(%v)", expireStr, err)
60 | return
61 | }
62 | node := myrpc.GetComet(key)
63 | if node == nil || node.CometRPC == nil {
64 | res["ret"] = app.NotFoundServer
65 | return
66 | }
67 | client := node.CometRPC.Get()
68 | if client == nil {
69 | res["ret"] = app.NotFoundServer
70 | return
71 | }
72 | rm := json.RawMessage(bodyBytes)
73 | msg, err := rm.MarshalJSON()
74 | if err != nil {
75 | res["ret"] = app.ParamErr
76 | logger.Errorf("json.RawMessage(\"%s\").MarshalJSON() error(%v)", body, err)
77 | return
78 | }
79 | args := &myrpc.CometPushPrivateArgs{Msg: json.RawMessage(msg), Expire: uint(expire), Key: key}
80 | ret := 0
81 | if err := client.Call(myrpc.CometServicePushPrivate, args, &ret); err != nil {
82 | logger.Errorf("client.Call(\"%s\", \"%v\", &ret) error(%v)", myrpc.CometServicePushPrivate, args, err)
83 | res["ret"] = app.InternalErr
84 | return
85 | }
86 | return
87 | }
88 |
89 | // DelPrivate handle for push private message.
90 | func DelPrivate(w http.ResponseWriter, r *http.Request) {
91 | if r.Method != "POST" {
92 | http.Error(w, "Method Not Allowed", 405)
93 | return
94 | }
95 | body := ""
96 | res := map[string]interface{}{"ret": app.OK}
97 | defer app.RetPWrite(w, r, res, &body, time.Now())
98 | // param
99 | bodyBytes, err := ioutil.ReadAll(r.Body)
100 | if err != nil {
101 | res["ret"] = app.ParamErr
102 | logger.Errorf("ioutil.ReadAll() failed (%s)", err.Error())
103 | return
104 | }
105 | body = string(bodyBytes)
106 | params, err := url.ParseQuery(body)
107 | if err != nil {
108 | logger.Errorf("url.ParseQuery(\"%s\") error(%v)", body, err)
109 | res["ret"] = app.ParamErr
110 | return
111 | }
112 | key := params.Get("key")
113 | if key == "" {
114 | res["ret"] = app.ParamErr
115 | return
116 | }
117 | client := myrpc.MessageRPC.Get()
118 | if client == nil {
119 | logger.Warnf("user_key: \"%s\" can't not find message rpc node", key)
120 | res["ret"] = app.InternalErr
121 | return
122 | }
123 | ret := 0
124 | if err := client.Call(myrpc.MessageServiceDelPrivate, key, &ret); err != nil {
125 | logger.Errorf("client.Call(\"%s\", \"%s\", &ret) error(%v)", myrpc.MessageServiceDelPrivate, key, err)
126 | res["ret"] = app.InternalErr
127 | return
128 | }
129 | return
130 | }
131 |
--------------------------------------------------------------------------------
/web/db.conf:
--------------------------------------------------------------------------------
1 | [base]
2 | #Mysql configuration
3 | app.dbURL root:123456@tcp(10.180.120.63:3308)/appmsgsrv?parseTime=true
4 | app.dbMaxIdleConns 10
5 | app.dbMaxOpenConns 100
--------------------------------------------------------------------------------
/web/handle.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "github.com/EPICPaaS/appmsgsrv/app"
21 | myrpc "github.com/EPICPaaS/appmsgsrv/rpc"
22 | "net/http"
23 | "strconv"
24 | "time"
25 | )
26 |
27 | // GetServer handle for server get
28 | func GetServer0(w http.ResponseWriter, r *http.Request) {
29 | if r.Method != "GET" {
30 | http.Error(w, "Method Not Allowed", 405)
31 | return
32 | }
33 | params := r.URL.Query()
34 | key := params.Get("key")
35 | callback := params.Get("callback")
36 | protoStr := params.Get("proto")
37 | res := map[string]interface{}{"ret": app.OK, "msg": "ok"}
38 | defer app.RetWrite(w, r, res, callback, time.Now())
39 | if key == "" {
40 | res["ret"] = app.ParamErr
41 | return
42 | }
43 | proto, err := strconv.Atoi(protoStr)
44 | if err != nil {
45 | logger.Errorf("strconv.Atoi(\"%s\") error(%v)", protoStr, err)
46 | res["ret"] = app.ParamErr
47 | return
48 | }
49 | // Match a push-server with the value computed through ketama algorithm
50 | node := myrpc.GetComet(key)
51 | if node == nil {
52 | res["ret"] = app.NotFoundServer
53 | return
54 | }
55 | addr := node.Addr[proto]
56 | if addr == nil || len(addr) == 0 {
57 | res["ret"] = app.NotFoundServer
58 | return
59 | }
60 | server := ""
61 | // Select the best ip
62 | if app.Conf.Router != "" {
63 | server = routerCN.SelectBest(r.RemoteAddr, addr)
64 | logger.Tracef("select the best ip:\"%s\" match with remoteAddr:\"%s\" , from ip list:\"%v\"", server, r.RemoteAddr, addr)
65 | }
66 | if server == "" {
67 | logger.Tracef("remote addr: \"%s\" chose the ip: \"%s\"", r.RemoteAddr, addr[0])
68 | server = addr[0]
69 | }
70 | res["data"] = map[string]interface{}{"server": server}
71 | return
72 | }
73 |
74 | // GetOfflineMsg get offline mesage http handler.
75 | func GetOfflineMsg0(w http.ResponseWriter, r *http.Request) {
76 | if r.Method != "GET" {
77 | http.Error(w, "Method Not Allowed", 405)
78 | return
79 | }
80 | params := r.URL.Query()
81 | key := params.Get("key")
82 | midStr := params.Get("mid")
83 | callback := params.Get("callback")
84 | res := map[string]interface{}{"ret": app.OK, "msg": "ok"}
85 | defer app.RetWrite(w, r, res, callback, time.Now())
86 | if key == "" || midStr == "" {
87 | res["ret"] = app.ParamErr
88 | return
89 | }
90 | mid, err := strconv.ParseInt(midStr, 10, 64)
91 | if err != nil {
92 | res["ret"] = app.ParamErr
93 | logger.Errorf("strconv.ParseInt(\"%s\", 10, 64) error(%v)", midStr, err)
94 | return
95 | }
96 | // RPC get offline messages
97 | reply := &myrpc.MessageGetResp{}
98 | args := &myrpc.MessageGetPrivateArgs{MsgId: mid, Key: key}
99 | client := myrpc.MessageRPC.Get()
100 | if client == nil {
101 | res["ret"] = app.InternalErr
102 | return
103 | }
104 | if err := client.Call(myrpc.MessageServiceGetPrivate, args, reply); err != nil {
105 | logger.Errorf("myrpc.MessageRPC.Call(\"%s\", \"%v\", reply) error(%v)", myrpc.MessageServiceGetPrivate, args, err)
106 | res["ret"] = app.InternalErr
107 | return
108 | }
109 | omsgs := []string{}
110 | opmsgs := []string{}
111 | for _, msg := range reply.Msgs {
112 | omsg, err := msg.OldBytes()
113 | if err != nil {
114 | res["ret"] = app.InternalErr
115 | return
116 | }
117 | omsgs = append(omsgs, string(omsg))
118 | }
119 |
120 | if len(omsgs) == 0 {
121 | return
122 | }
123 |
124 | res["data"] = map[string]interface{}{"msgs": omsgs, "pmsgs": opmsgs}
125 | return
126 | }
127 |
128 | // GetTime get server time http handler.
129 | func GetTime0(w http.ResponseWriter, r *http.Request) {
130 | if r.Method != "GET" {
131 | http.Error(w, "Method Not Allowed", 405)
132 | return
133 | }
134 | params := r.URL.Query()
135 | callback := params.Get("callback")
136 | res := map[string]interface{}{"ret": app.OK, "msg": "ok"}
137 | now := time.Now()
138 | defer app.RetWrite(w, r, res, callback, now)
139 | res["data"] = map[string]interface{}{"timeid": now.UnixNano() / 100}
140 | return
141 | }
142 |
--------------------------------------------------------------------------------
/web/handle_1.0.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "net/http"
21 | "strconv"
22 | "time"
23 |
24 | "github.com/EPICPaaS/appmsgsrv/app"
25 | myrpc "github.com/EPICPaaS/appmsgsrv/rpc"
26 | )
27 |
28 | // GetServer handle for server get
29 | func GetServer(w http.ResponseWriter, r *http.Request) {
30 | if r.Method != "GET" {
31 | http.Error(w, "Method Not Allowed", 405)
32 | return
33 | }
34 | params := r.URL.Query()
35 | key := params.Get("k")
36 | callback := params.Get("cb")
37 | protoStr := params.Get("p")
38 | res := map[string]interface{}{"ret": app.OK}
39 | defer app.RetWrite(w, r, res, callback, time.Now())
40 | if key == "" {
41 | res["ret"] = app.ParamErr
42 | return
43 | }
44 | proto, err := strconv.Atoi(protoStr)
45 | if err != nil {
46 | logger.Errorf("strconv.Atoi(\"%s\") error(%v)", protoStr, err)
47 | res["ret"] = app.ParamErr
48 | return
49 | }
50 | // Match a push-server with the value computed through ketama algorithm
51 | node := myrpc.GetComet(key)
52 | if node == nil {
53 | res["ret"] = app.NotFoundServer
54 | return
55 | }
56 | addr := node.Addr[proto]
57 | if addr == nil || len(addr) == 0 {
58 | res["ret"] = app.NotFoundServer
59 | return
60 | }
61 | server := ""
62 | // Select the best ip
63 | if app.Conf.Router != "" {
64 | server = routerCN.SelectBest(r.RemoteAddr, addr)
65 | logger.Tracef("select the best ip:\"%s\" match with remoteAddr:\"%s\" , from ip list:\"%v\"", server, r.RemoteAddr, addr)
66 | }
67 | if server == "" {
68 | logger.Tracef("remote addr: \"%s\" chose the ip: \"%s\"", r.RemoteAddr, addr[0])
69 | server = addr[0]
70 | }
71 | res["data"] = map[string]interface{}{"server": server}
72 | return
73 | }
74 |
75 | // GetOfflineMsg get offline mesage http handler.
76 | func GetOfflineMsg(w http.ResponseWriter, r *http.Request) {
77 | if r.Method != "GET" {
78 | http.Error(w, "Method Not Allowed", 405)
79 | return
80 | }
81 | params := r.URL.Query()
82 | key := params.Get("k")
83 | midStr := params.Get("m")
84 | callback := params.Get("cb")
85 | res := map[string]interface{}{"ret": app.OK}
86 | defer app.RetWrite(w, r, res, callback, time.Now())
87 | if key == "" || midStr == "" {
88 | res["ret"] = app.ParamErr
89 | return
90 | }
91 | mid, err := strconv.ParseInt(midStr, 10, 64)
92 | if err != nil {
93 | res["ret"] = app.ParamErr
94 | logger.Errorf("strconv.ParseInt(\"%s\", 10, 64) error(%v)", midStr, err)
95 | return
96 | }
97 | // RPC get offline messages
98 | reply := &myrpc.MessageGetResp{}
99 | args := &myrpc.MessageGetPrivateArgs{MsgId: mid, Key: key}
100 | client := myrpc.MessageRPC.Get()
101 | if client == nil {
102 | res["ret"] = app.InternalErr
103 | return
104 | }
105 | if err := client.Call(myrpc.MessageServiceGetPrivate, args, reply); err != nil {
106 | logger.Errorf("myrpc.MessageRPC.Call(\"%s\", \"%v\", reply) error(%v)", myrpc.MessageServiceGetPrivate, args, err)
107 | res["ret"] = app.InternalErr
108 | return
109 | }
110 | if len(reply.Msgs) == 0 {
111 | return
112 | }
113 | res["data"] = map[string]interface{}{"msgs": reply.Msgs}
114 | return
115 | }
116 |
117 | // GetTime get server time http handler.
118 | func GetTime(w http.ResponseWriter, r *http.Request) {
119 | if r.Method != "GET" {
120 | http.Error(w, "Method Not Allowed", 405)
121 | return
122 | }
123 | params := r.URL.Query()
124 | callback := params.Get("cb")
125 | res := map[string]interface{}{"ret": app.OK}
126 | now := time.Now()
127 | defer app.RetWrite(w, r, res, callback, now)
128 | res["data"] = map[string]interface{}{"timeid": now.UnixNano() / 1000}
129 | return
130 | }
131 |
--------------------------------------------------------------------------------
/web/main.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "flag"
21 | "os"
22 | "runtime"
23 | "time"
24 |
25 | "github.com/EPICPaaS/appmsgsrv/app"
26 | "github.com/EPICPaaS/appmsgsrv/db"
27 | "github.com/EPICPaaS/appmsgsrv/perf"
28 | "github.com/EPICPaaS/appmsgsrv/process"
29 | "github.com/EPICPaaS/appmsgsrv/ver"
30 |
31 | "github.com/b3log/wide/log"
32 | )
33 |
34 | var logger = log.NewLogger(os.Stdout)
35 |
36 | func main() {
37 | var err error
38 | // Parse cmd-line arguments
39 | logLevel := flag.String("log_level", "info", "logger level")
40 | flag.Parse()
41 | log.SetLevel(*logLevel)
42 |
43 | logger.Infof("web ver: \"%s\" start", ver.Version)
44 |
45 | if err = app.InitConfig(); err != nil {
46 | logger.Errorf("InitConfig() error(%v)", err)
47 | return
48 | }
49 | //init db config
50 | if err = db.InitConfig(); err != nil {
51 | logger.Error("db-InitConfig() error(%v)", err)
52 | return
53 | }
54 | // Set max routine
55 | runtime.GOMAXPROCS(app.Conf.MaxProc)
56 | // init zookeeper
57 | zkConn, err := InitZK()
58 | if err != nil {
59 | logger.Errorf("InitZookeeper() error(%v)", err)
60 | return
61 | }
62 | // if process exit, close zk
63 | defer zkConn.Close()
64 | // start pprof http
65 | perf.Init(app.Conf.PprofBind)
66 | // Init network router
67 | if app.Conf.Router != "" {
68 | if err := InitRouter(); err != nil {
69 | logger.Errorf("InitRouter() failed(%v)", err)
70 | return
71 | }
72 | }
73 |
74 | db.InitDB()
75 | defer db.CloseDB()
76 |
77 | app.InitRedisStorage()
78 |
79 | // start http listen.
80 | StartHTTP()
81 | // init process
82 | // sleep one second, let the listen start
83 | time.Sleep(time.Second)
84 | if err = process.Init(app.Conf.User, app.Conf.Dir, app.Conf.PidFile); err != nil {
85 | logger.Errorf("process.Init() error(%v)", err)
86 | return
87 | }
88 |
89 | defer db.MySQL.Close()
90 |
91 | //初始化配额配置
92 | app.InitQuotaAll()
93 | go app.LoadQuotaAll()
94 |
95 | //启动扫描过期文件
96 | go app.ScanExpireFileLink()
97 |
98 | // init signals, block wait signals
99 | signalCH := InitSignal()
100 | HandleSignal(signalCH)
101 |
102 | logger.Info("web stop")
103 | }
104 |
--------------------------------------------------------------------------------
/web/router.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "fmt"
21 | "github.com/EPICPaaS/appmsgsrv/app"
22 | "github.com/EPICPaaS/appmsgsrv/router"
23 | )
24 |
25 | const (
26 | NetworkRouterCN = "CN" // china
27 | )
28 |
29 | var (
30 | routerCN *router.RouterCN
31 | )
32 |
33 | // Init network router
34 | func InitRouter() error {
35 | switch app.Conf.Router {
36 | case NetworkRouterCN:
37 | r, err := router.InitCN(app.Conf.QQWryPath)
38 | if err != nil {
39 | logger.Errorf("init china network router failed(%v)", err)
40 | return err
41 | }
42 | routerCN = r
43 | default:
44 | // TODO:support more countries` network routers
45 | return fmt.Errorf("unknown network router:\"%s\", please check your configuration", app.Conf.Router)
46 | }
47 |
48 | return nil
49 | }
50 |
--------------------------------------------------------------------------------
/web/signal.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2014 Terry Mao, LiuDing All rights reserved.
2 | // This file is part of gopush-cluster.
3 |
4 | // gopush-cluster is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 |
9 | // gopush-cluster is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 |
14 | // You should have received a copy of the GNU General Public License
15 | // along with gopush-cluster. If not, see .
16 |
17 | package main
18 |
19 | import (
20 | "os"
21 | "os/signal"
22 | "syscall"
23 | )
24 |
25 | // InitSignal register signals handler.
26 | func InitSignal() chan os.Signal {
27 | c := make(chan os.Signal, 1)
28 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGSTOP)
29 | return c
30 | }
31 |
32 | // HandleSignal fetch signal from chan then do exit or reload.
33 | func HandleSignal(c chan os.Signal) {
34 | // Block until a signal is received.
35 | for {
36 | s := <-c
37 | logger.Infof("get a signal %s", s.String())
38 | switch s {
39 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT:
40 | logger.Error("Comet Exit: syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGINT")
41 | return
42 | case syscall.SIGHUP:
43 | // TODO reload
44 | //return
45 | logger.Error("Comet Exit: syscall.SIGHUP")
46 | default:
47 | logger.Error("Comet Exit: default")
48 | return
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/web/static/css/user.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | body, div, p, ul, li, dl, dt, dd, h1, h2, h3, h4, h5, h6 {
4 | margin:0;
5 | padding:0;
6 | }
7 | html {
8 | font-size: 10px;
9 | }
10 | body {
11 | background:#eaeaea;
12 | font-family: 微软雅黑, "Helvetica Neue", Helvetica, STHeiTi, sans-serif;
13 | }
14 | /*html, body {
15 | -webkit-user-select: none;
16 | user-select: none;
17 | }*/
18 | img {
19 | border:none;
20 | margin:0;
21 | padding:0;
22 | }
23 | a {
24 | text-decoration:none;
25 | }
26 | a, img {
27 | -webkit-touch-callout: none; /* 禁止长按链接与图片弹出菜单 */
28 | }
29 | #warp {
30 | width:100%;
31 | max-width:1080px;
32 | margin:0 auto;
33 | font-size: 2.4rem;
34 | }
35 | #warp header {
36 |
37 | }
38 | header h1 {
39 | font-size: 2.8rem;
40 | font-weight: normal;
41 | padding:0px 0px 10px 10px;
42 | background: url(../images/line-big.png) left bottom no-repeat;
43 | background-size: 100% 2px;
44 | }
45 | .section {
46 | margin-top: 25px;
47 | padding: 10px;
48 | background: #fbfbfc;
49 | }
50 | #head-photo {
51 | margin: 35px 0px 25px 0px;
52 | display: box;
53 | display: -webkit-box;
54 | display: -moz-box;
55 | -webkit-box-pack:center;
56 | -moz-box-pack:center;
57 | box-pack:center;
58 | width: 100%;
59 | }
60 | #img-wrap {
61 | width: 13.5%;
62 | max-width: 144px;
63 | }
64 | #head-photo img {
65 | box-shadow: 0px 0px 5px 3px #dddcdc;
66 | border-radius: 65px;
67 | max-width: 100%;
68 | width: 100%;
69 | }
70 | #user-name {
71 | text-align: center;
72 | margin-bottom: 35px;
73 | }
74 | #user-info .section {
75 | padding: 20px;
76 | }
77 | footer {
78 | margin: 55px 0px 20px 0px;
79 | color: #8b8b8b;
80 | font-size: 14px;
81 | text-align: center;
82 | }
83 |
84 | @media screen and (max-width: 480px) {
85 | html {
86 | font-size: 7.5px;
87 | }
88 | }
--------------------------------------------------------------------------------
/web/static/images/head-photo-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/web/static/images/head-photo-big.png
--------------------------------------------------------------------------------
/web/static/images/line-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EPICPaaS/appmsgsrv/537dd8c8fa6b4c4211a225ffd1b215553d58d876/web/static/images/line-big.png
--------------------------------------------------------------------------------
/web/view/erweima.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 用户信息
7 |
8 |
9 |
10 |
11 |
12 |
13 | 用户信息
14 |
15 |
18 | {{.nickname}}
19 |
20 |
21 |
Name: {{.nickname}}
22 |
Email:{{.email}}
23 |
Phone:{{.phone}}
24 |
25 |
26 |
31 |
32 |
33 |