├── .bowerrc ├── .gitignore ├── Godeps ├── Godeps.json └── Readme ├── README.md ├── bower.json ├── build ├── config.yaml.example ├── config └── config.go ├── controller ├── controller.go └── sort.go ├── memcached ├── cmd.go └── connection.go ├── middleman ├── manager │ └── manager.go └── middleman │ ├── default.go │ ├── unserialize_to_json.php │ └── yii.go ├── mu.go ├── sample.png └── ui ├── assets └── index.js └── templates ├── cluster.html └── node.html /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "ui/assets/lib/", 3 | "analytics": false, 4 | "timeout": 120000 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ui/assets/lib/ 2 | Godeps/_workspace/ 3 | *.exe 4 | config.yaml 5 | memcached-ui 6 | mu 7 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/youngsterxyf/memcached-ui", 3 | "GoVersion": "go1.5.1", 4 | "Deps": [ 5 | { 6 | "ImportPath": "github.com/gin-gonic/gin", 7 | "Comment": "v1.0rc1-143-g704d690", 8 | "Rev": "704d690ac0e25fcd060ff82f75f913e879e65a3c" 9 | }, 10 | { 11 | "ImportPath": "github.com/manucorporat/sse", 12 | "Rev": "fe6ea2c8e398672518ef204bf0fbd9af858d0e15" 13 | }, 14 | { 15 | "ImportPath": "github.com/mattn/go-colorable", 16 | "Rev": "40e4aedc8fabf8c23e040057540867186712faa5" 17 | }, 18 | { 19 | "ImportPath": "golang.org/x/net/context", 20 | "Rev": "972f0c5fbe4ae29e666c3f78c3ed42ae7a448b0a" 21 | }, 22 | { 23 | "ImportPath": "gopkg.in/bluesuncorp/validator.v5", 24 | "Comment": "v5.11", 25 | "Rev": "98121ac23ff3d764f525d18d8de944d150e3c0fe" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Memcached UI 2 | 3 | 一个简单实用的Memcached Web客户端。 4 | 5 | - 以插件方式支持多种Web框架使用Memached的方式,比如Yii框架会对原始的键值进行处理,存入Memcached的键是一个哈希值,而value只是序列化后的结果。 6 | - 不依赖第三方Memcached客户端库 7 | - 支持Basic Auth身份认证方式 8 | 9 | ------ 10 | 11 | 插件的实现见代码文件:`middleman/middleman/default.go` 和 `middleman/middleman/yii.go`。 12 | 13 | ------ 14 | 15 | 界面: 16 | 17 | ![memcached-ui](https://raw.github.com/youngsterxyf/memcached-ui/master/sample.png) 18 | 19 | 部署: 20 | 21 | 1. `go get github.com/youngsterxyf/memcached-ui` 22 | 2. `cd $GOPATH/src/github.com/youngsterxyf/memcached-ui` 23 | 3. `./build` 24 | 4. `bower install` 25 | 5. `cp config.yaml.example config.yaml`,并根据需求修改配置信息 26 | 6. `nohup ./mu > mu.log 2>&1 &` 27 | 7. 访问 http://127.0.0.1:8080 28 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memcached-ui", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/youngsterxyf/memcached-ui", 5 | "authors": [ 6 | "youngsterxyf " 7 | ], 8 | "description": "", 9 | "main": "", 10 | "moduleType": [], 11 | "license": "MIT", 12 | "private": true, 13 | "ignore": [ 14 | "**/.*", 15 | "node_modules", 16 | "bower_components", 17 | "ui/assets/lib/", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": { 22 | "vue": "~0.12.16", 23 | "bootstrap": "~3.3.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | pwd=$PWD 4 | export PATH=$GOPATH/bin:$GOROOT/bin:$PATH 5 | 6 | eval $(go env) 7 | 8 | GIT_SHA=`git rev-parse --short HEAD || echo "GitNotFound"` 9 | GIT_BRANCH=`git branch 2>/dev/null | grep "^\*" | sed -e "s/^\*\ //"` 10 | if [ "${GIT_BRANCH}" != "" ]; then 11 | GIT_SHA=$GIT_SHA"($GIT_BRANCH)" 12 | fi 13 | WHEN=`date '+%Y-%m-%d_%H:%M:%S'` 14 | 15 | CGO_ENABLED=0 go build -installsuffix -a -v -ldflags "-X main.GitSHA=${GIT_SHA} -X main.BuildTime=${WHEN}" -o mu 16 | -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | instances: 2 | localhost-Yii: 3 | source: localhost:11211 4 | middelman_name: yii 5 | middleman_config: 6 | appName: gxt 7 | hash: yes 8 | php_bin: php 9 | unserialize_script: ./middleman/middleman/unserialize_to_json.php 10 | basic_auth: 11 | "on": true 12 | username: test 13 | password: test 14 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type InstanceConfig struct { 10 | Source string `yaml:"source"` 11 | MiddlemanName string `yaml:"middleman_name"` 12 | MiddlemanConfig map[string]string `yaml:"middleman_config"` 13 | } 14 | 15 | type BasicAuthConfig struct { 16 | On bool `yaml:"on"` 17 | Username string `yaml:"username"` 18 | Password string `yaml:"password"` 19 | } 20 | 21 | type AppConfigStruct struct { 22 | Instances map[string]InstanceConfig `yaml:"instances"` 23 | Basic_auth BasicAuthConfig `yaml:"basic_auth"` 24 | } 25 | 26 | var DefaultAppConfig = AppConfigStruct{ 27 | Instances: map[string]InstanceConfig{ 28 | "localhost-Yii": InstanceConfig{ 29 | Source: "localhost:11211", 30 | MiddlemanName: "yii", 31 | MiddlemanConfig: map[string]string{ 32 | "appName": "gxt", 33 | "hash": "yes", 34 | "php_bin": "php", 35 | "unserialize_script": "./middleman/middleman/unserialize_to_json.php", 36 | }, 37 | }, 38 | }, 39 | Basic_auth: BasicAuthConfig{ 40 | On: true, 41 | Username: "test", 42 | Password: "test", 43 | }, 44 | } 45 | 46 | func LoadAppConfig(configPath string) (AppConfigStruct, error) { 47 | confContent, err := ioutil.ReadFile(configPath) 48 | if err != nil { 49 | return DefaultAppConfig, err 50 | } 51 | var conf AppConfigStruct 52 | if err := yaml.Unmarshal(confContent, &conf); err != nil { 53 | return DefaultAppConfig, err 54 | } 55 | return conf, nil 56 | } 57 | -------------------------------------------------------------------------------- /controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/youngsterxyf/memcached-ui/config" 13 | "github.com/youngsterxyf/memcached-ui/memcached" 14 | MiddlemanManager "github.com/youngsterxyf/memcached-ui/middleman/manager" 15 | _ "github.com/youngsterxyf/memcached-ui/middleman/middleman" 16 | ) 17 | 18 | type StatsInfoStruct struct { 19 | InstanceID string 20 | Source string 21 | Pid string 22 | Version string 23 | Uptime string 24 | MaxMemoryLimit string 25 | CurrMemoryUsage string 26 | CurrItems string 27 | CurrConnections string 28 | GetHits string 29 | GetMisses string 30 | GetRate string 31 | } 32 | 33 | var actionAllowed []string = []string{"get", "set", "delete", "flush_all"} 34 | 35 | func validAction(targetAction string) bool { 36 | for _, action := range actionAllowed { 37 | if targetAction == action { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | func getAppConfig(c *gin.Context) config.AppConfigStruct { 45 | appConf, _ := c.Get("app_conf") 46 | return appConf.(config.AppConfigStruct) 47 | } 48 | 49 | func newMemcached(server string) (memcached.Memcached, error) { 50 | serverParts := strings.Split(server, ":") 51 | host := serverParts[0] 52 | port, _ := strconv.Atoi(serverParts[1]) 53 | 54 | m := memcached.Memcached{} 55 | err := m.New(host, port) 56 | return m, err 57 | } 58 | 59 | func getStatsInfo(server string) (map[string]string, error) { 60 | m, err := newMemcached(server) 61 | if err != nil { 62 | return nil, err 63 | } 64 | defer m.Close() 65 | return m.Stats() 66 | } 67 | 68 | func formatUptime(uptime int) string { 69 | day := uptime / 86400 70 | hour := uptime % 86400 / 3600 71 | minute := uptime % 3600 / 60 72 | second := uptime % 60 73 | return fmt.Sprintf("%d天%d时%d分%d秒", day, hour, minute, second) 74 | } 75 | 76 | func formatMemoryUsage(usageBytes int) string { 77 | return ToHuman(int64(usageBytes)) 78 | } 79 | 80 | func statsMap2Struct(statsMapper map[string]string) StatsInfoStruct { 81 | uptime, _ := strconv.Atoi(statsMapper["uptime"]) 82 | maxMemoryLimit, _ := strconv.Atoi(statsMapper["limit_maxbytes"]) 83 | currMemoryUsage, _ := strconv.Atoi(statsMapper["bytes"]) 84 | 85 | GetHits := statsMapper["get_hits"] 86 | GetMisses := statsMapper["get_misses"] 87 | GetRate := "0" 88 | 89 | h, err := strconv.Atoi(GetHits) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | m, err := strconv.Atoi(GetMisses) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | 98 | GetCount := m + h 99 | if GetCount > 0 { 100 | GetRate = strconv.FormatFloat(float64(h)/float64(GetCount)*100, 'f', 1, 64) 101 | } 102 | 103 | return StatsInfoStruct{ 104 | Pid: statsMapper["pid"], 105 | Version: statsMapper["version"], 106 | Uptime: formatUptime(uptime), 107 | MaxMemoryLimit: formatMemoryUsage(maxMemoryLimit), 108 | CurrMemoryUsage: formatMemoryUsage(currMemoryUsage), 109 | CurrItems: statsMapper["curr_items"], 110 | CurrConnections: statsMapper["curr_connections"], 111 | GetHits: GetHits, 112 | GetMisses: GetMisses, 113 | GetRate: GetRate, 114 | } 115 | } 116 | 117 | func Home(c *gin.Context) { 118 | c.Redirect(http.StatusMovedPermanently, "/cluster") 119 | } 120 | 121 | func Node(c *gin.Context) { 122 | ac := getAppConfig(c) 123 | 124 | var instances []string 125 | for k, _ := range ac.Instances { 126 | instances = append(instances, k) 127 | } 128 | // sort hosts to fix issue: 129 | // 10.2.96.13 130 | // 10.2.96.130 131 | // 10.2.96.14 132 | sort.Sort(Hosts(instances)) 133 | 134 | instanceID := c.Query("instance") 135 | if _, ok := ac.Instances[instanceID]; ok == false { 136 | instanceID = instances[0] 137 | } 138 | targetSource := ac.Instances[instanceID].Source 139 | 140 | var structedStatsInfo StatsInfoStruct 141 | 142 | infoErr := "" 143 | hasInfoErr := false 144 | statsInfo, err := getStatsInfo(targetSource) 145 | if err != nil { 146 | infoErr = err.Error() 147 | hasInfoErr = true 148 | } else { 149 | structedStatsInfo = statsMap2Struct(statsInfo) 150 | } 151 | 152 | structedStatsInfo.InstanceID = instanceID 153 | structedStatsInfo.Source = targetSource 154 | 155 | c.HTML(http.StatusOK, "node.html", gin.H{ 156 | "HasInfoErr": hasInfoErr, 157 | "InfoErr": infoErr, 158 | "Instances": instances, 159 | "StatsInfo": structedStatsInfo, 160 | }) 161 | } 162 | 163 | func Cluster(c *gin.Context) { 164 | ac := getAppConfig(c) 165 | 166 | var infoErrs []string 167 | var instances []string 168 | statsInofMap := make(map[string]StatsInfoStruct) 169 | for k, instance := range ac.Instances { 170 | instances = append(instances, k) 171 | 172 | var structedStatsInfo StatsInfoStruct 173 | statsInfo, err := getStatsInfo(instance.Source) 174 | if err != nil { 175 | infoErrs = append(infoErrs, err.Error()) 176 | } else { 177 | structedStatsInfo = statsMap2Struct(statsInfo) 178 | } 179 | 180 | structedStatsInfo.InstanceID = k 181 | structedStatsInfo.Source = instance.Source 182 | 183 | statsInofMap[k] = structedStatsInfo 184 | } 185 | sort.Sort(Hosts(instances)) 186 | 187 | var statsInofs []StatsInfoStruct 188 | for _, k := range instances { 189 | statInfo := statsInofMap[k] 190 | statsInofs = append(statsInofs, statInfo) 191 | } 192 | 193 | c.HTML(http.StatusOK, "cluster.html", gin.H{ 194 | "InfoErrs": infoErrs, 195 | "Instances": instances, 196 | "StatsInfos": statsInofs, 197 | }) 198 | } 199 | 200 | func Do(c *gin.Context) { 201 | ac := getAppConfig(c) 202 | 203 | instanceID := c.PostForm("instance") 204 | if _, ok := ac.Instances[instanceID]; ok == false { 205 | c.JSON(http.StatusOK, gin.H{ 206 | "status": "failure", 207 | "msg": "不存在目标应用", 208 | }) 209 | return 210 | } 211 | targetAction := c.PostForm("action") 212 | if validAction(targetAction) == false { 213 | c.JSON(http.StatusOK, gin.H{ 214 | "status": "failure", 215 | "msg": "不存在目标action", 216 | }) 217 | return 218 | } 219 | m, err := newMemcached(ac.Instances[instanceID].Source) 220 | if err != nil { 221 | c.JSON(http.StatusOK, gin.H{ 222 | "status": "failure", 223 | "msg": "目标Memcached服务连接失败:" + err.Error(), 224 | }) 225 | return 226 | } 227 | defer m.Close() 228 | 229 | targetInstanceConfig := ac.Instances[instanceID] 230 | targetMiddleman := MiddlemanManager.Get(targetInstanceConfig.MiddlemanName, targetInstanceConfig.MiddlemanConfig) 231 | if targetMiddleman == nil { 232 | targetMiddleman = MiddlemanManager.Get("default", nil) 233 | } 234 | 235 | switch { 236 | case targetAction == "get": 237 | key := targetMiddleman.GenInnerKey(c.PostForm("key")) 238 | resp, err := m.Get(key) 239 | if err != nil { 240 | c.JSON(http.StatusOK, gin.H{ 241 | "status": "failure", 242 | "msg": "获取缓存数据失败:" + err.Error(), 243 | }) 244 | return 245 | } 246 | 247 | c.JSON(http.StatusOK, gin.H{ 248 | "status": "success", 249 | "data": targetMiddleman.UnserializeValue(resp), 250 | }) 251 | return 252 | case targetAction == "set": 253 | key := targetMiddleman.GenInnerKey(c.PostForm("key")) 254 | value := targetMiddleman.SerializeValue(c.PostForm("value")) 255 | expTime := c.DefaultPostForm("exp_time", "0") 256 | expTimeInt, err := strconv.Atoi(expTime) 257 | if err != nil { 258 | expTimeInt = 0 259 | } 260 | resp, err := m.Set(memcached.StorageCmdArgStruct{"key": key, "value": value, "expire_time": expTimeInt}) 261 | if err != nil { 262 | c.JSON(http.StatusOK, gin.H{ 263 | "status": "failure", 264 | "msg": "添加缓存失败:" + err.Error(), 265 | }) 266 | return 267 | } 268 | c.JSON(http.StatusOK, gin.H{ 269 | "status": "success", 270 | "data": string(resp), 271 | }) 272 | case targetAction == "delete": 273 | key := targetMiddleman.GenInnerKey(c.PostForm("key")) 274 | resp, err := m.Delete(key) 275 | if err != nil { 276 | c.JSON(http.StatusOK, gin.H{ 277 | "status": "failure", 278 | "msg": "删除缓存失败:" + err.Error(), 279 | }) 280 | return 281 | } 282 | c.JSON(http.StatusOK, gin.H{ 283 | "status": "success", 284 | "data": string(resp), 285 | }) 286 | case targetAction == "flush_all": 287 | resp, err := m.FlushAll() 288 | if err != nil { 289 | c.JSON(http.StatusOK, gin.H{ 290 | "status": "failure", 291 | "msg": "清空缓存失败:" + err.Error(), 292 | }) 293 | return 294 | } 295 | c.JSON(http.StatusOK, gin.H{ 296 | "status": "success", 297 | "data": string(resp), 298 | }) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /controller/sort.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | kb, mb, gb, tb, pb float64 11 | ) 12 | 13 | func init() { 14 | kb = 1024 15 | mb = 1024 * kb 16 | gb = 1024 * mb 17 | tb = 1024 * gb 18 | pb = 1024 * tb 19 | } 20 | 21 | func ToHuman(size int64) string { 22 | fsize := float64(size) 23 | var negative bool 24 | if fsize < 0 { 25 | fsize = -fsize 26 | negative = true 27 | } 28 | 29 | var toHuman string 30 | switch { 31 | case fsize > pb: 32 | toHuman = fmt.Sprintf("%.3f PB", fsize/pb) 33 | case fsize > tb: 34 | toHuman = fmt.Sprintf("%.3f TB", fsize/tb) 35 | case fsize > gb: 36 | toHuman = fmt.Sprintf("%.3f GB", fsize/gb) 37 | case fsize > mb: 38 | toHuman = fmt.Sprintf("%.3f MB", fsize/mb) 39 | case fsize > kb: 40 | toHuman = fmt.Sprintf("%.3f KB", fsize/kb) 41 | default: 42 | toHuman = fmt.Sprintf("%d B", size) 43 | } 44 | if negative { 45 | toHuman = fmt.Sprintf("-%s", toHuman) 46 | } 47 | return toHuman 48 | } 49 | 50 | type Hosts []string 51 | 52 | func (h Hosts) Len() int { 53 | return len(h) 54 | } 55 | func (h Hosts) Swap(i, j int) { 56 | h[i], h[j] = h[j], h[i] 57 | } 58 | func (h Hosts) Less(i, j int) bool { 59 | return hostLess(h[i], h[j]) 60 | } 61 | func hostLess(host1, host2 string) bool { 62 | host1s := strings.SplitN(host1, ":", 2) 63 | host1 = host1s[0] 64 | host2s := strings.SplitN(host2, ":", 2) 65 | host2 = host2s[0] 66 | parts1 := strings.SplitN(host1, ".", 5) 67 | parts2 := strings.SplitN(host2, ".", 5) 68 | if len(parts1) < 4 || len(parts2) < 4 { 69 | return host1 < host2 70 | } 71 | for i := 0; i < 4; i++ { 72 | ipInt1, _ := strconv.Atoi(parts1[i]) 73 | ipInt2, _ := strconv.Atoi(parts2[i]) 74 | if ipInt1 == ipInt2 { 75 | continue 76 | } else { 77 | return ipInt1 < ipInt2 78 | } 79 | } 80 | return false 81 | } 82 | 83 | type ValSorter struct { 84 | Keys []string 85 | Vals []int 86 | } 87 | 88 | func NewValSorter(m map[string]int) *ValSorter { 89 | vs := &ValSorter{ 90 | Keys: make([]string, 0, len(m)), 91 | Vals: make([]int, 0, len(m)), 92 | } 93 | for k, v := range m { 94 | vs.Keys = append(vs.Keys, k) 95 | vs.Vals = append(vs.Vals, v) 96 | } 97 | return vs 98 | } 99 | func (vs *ValSorter) Len() int { return len(vs.Vals) } 100 | func (vs *ValSorter) Less(i, j int) bool { return vs.Vals[i] < vs.Vals[j] } 101 | func (vs *ValSorter) Swap(i, j int) { 102 | vs.Vals[i], vs.Vals[j] = vs.Vals[j], vs.Vals[i] 103 | vs.Keys[i], vs.Keys[j] = vs.Keys[j], vs.Keys[i] 104 | } 105 | -------------------------------------------------------------------------------- /memcached/cmd.go: -------------------------------------------------------------------------------- 1 | package memcached 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // https://github.com/memcached/memcached/blob/master/doc/protocol.txt 11 | 12 | const ( 13 | // magic number,目前没啥鸟用 14 | SET_FLAGS = 123456 15 | ) 16 | 17 | type Memcached struct { 18 | conn Connection 19 | } 20 | 21 | func (m *Memcached) New(host string, port int) error { 22 | m.conn = Connection{ 23 | Host: host, 24 | Port: port, 25 | } 26 | return m.conn.Open() 27 | } 28 | 29 | /* 30 | 存储类型命令:set、add、replace、append、prepend、cas 31 | */ 32 | 33 | type StorageCmdArgStruct map[string]interface{} 34 | 35 | func (m *Memcached) runStorageCmd(cmdName string, args StorageCmdArgStruct) ([]byte, error) { 36 | // 必须 37 | var key, value string 38 | keyI, ok := args["key"] 39 | if ok == false { 40 | return nil, errors.New("缺少参数key") 41 | } else { 42 | key = keyI.(string) 43 | } 44 | valueI, ok := args["value"] 45 | if ok == false { 46 | return nil, errors.New("缺少参数value") 47 | } else { 48 | value = valueI.(string) 49 | } 50 | 51 | // 可选 52 | var flags, expTime string 53 | flagsI, ok := args["flags"] 54 | if ok == false { 55 | flags = strconv.Itoa(SET_FLAGS) 56 | } else { 57 | flags = string(flagsI.(int)) 58 | } 59 | expTimeI, ok := args["expire_time"] 60 | if ok == false { 61 | expTime = "0" 62 | } else { 63 | expTime = strconv.Itoa(expTimeI.(int)) 64 | } 65 | argList := []string{key, flags, expTime, strconv.Itoa(len(value))} 66 | if cmdName == "cas" { 67 | if casUnique, ok := args["cas_unique"]; ok { 68 | argList = append(argList, casUnique.(string)) 69 | } 70 | } 71 | 72 | cmd := fmt.Sprintf("%s %s\r\n", cmdName, strings.Join(argList, " ")) 73 | err := m.conn.Send(cmd, fmt.Sprintf("%s\r\n", value)) 74 | if err != nil { 75 | return nil, err 76 | } 77 | resp, err := m.conn.Receive(cmdName) 78 | return resp.([]byte), err 79 | } 80 | 81 | func (m *Memcached) Set(args StorageCmdArgStruct) ([]byte, error) { 82 | return m.runStorageCmd("set", args) 83 | } 84 | 85 | func (m *Memcached) Add(args StorageCmdArgStruct) ([]byte, error) { 86 | return m.runStorageCmd("add", args) 87 | } 88 | 89 | func (m *Memcached) Replace(args StorageCmdArgStruct) ([]byte, error) { 90 | return m.runStorageCmd("replace", args) 91 | } 92 | 93 | func (m *Memcached) Append(args StorageCmdArgStruct) ([]byte, error) { 94 | return m.runStorageCmd("append", args) 95 | } 96 | 97 | func (m *Memcached) Prepend(args StorageCmdArgStruct) ([]byte, error) { 98 | return m.runStorageCmd("prepend", args) 99 | } 100 | 101 | func (m *Memcached) Cas(args StorageCmdArgStruct) ([]byte, error) { 102 | return m.runStorageCmd("cas", args) 103 | } 104 | 105 | /* 106 | 数据获取类型命令:get、gets 107 | */ 108 | 109 | func (m *Memcached) runFetchCmd(cmdName, keys string) (map[string]string, error) { 110 | cmd := fmt.Sprintf("%s %s\r\n", cmdName, keys) 111 | err := m.conn.Send(cmd) 112 | if err != nil { 113 | return nil, err 114 | } 115 | resp, err := m.conn.Receive(cmdName) 116 | return resp.(map[string]string), err 117 | } 118 | 119 | func (m *Memcached) Get(key string) (string, error) { 120 | resp, err := m.runFetchCmd("get", key) 121 | if err != nil { 122 | return "", err 123 | } 124 | return resp[key], nil 125 | } 126 | 127 | func (m *Memcached) Gets(keys ...string) (map[string]string, error) { 128 | resp, err := m.runFetchCmd("gets", strings.Join(keys, " ")) 129 | if err != nil { 130 | return nil, err 131 | } 132 | return resp, nil 133 | } 134 | 135 | /* 136 | 其他命令:flush_all、delete、incr、decr、touch、stats 137 | */ 138 | 139 | func (m *Memcached) FlushAll() ([]byte, error) { 140 | cmd := "flush_all\r\n" 141 | err := m.conn.Send(cmd) 142 | if err != nil { 143 | return nil, err 144 | } 145 | resp, err := m.conn.Receive("flush_all") 146 | return resp.([]byte), err 147 | } 148 | 149 | func (m *Memcached) Delete(key string) ([]byte, error) { 150 | cmd := fmt.Sprintf("delete %s\r\n", key) 151 | err := m.conn.Send(cmd) 152 | if err != nil { 153 | return nil, err 154 | } 155 | resp, err := m.conn.Receive("delete") 156 | return resp.([]byte), err 157 | } 158 | 159 | func (m *Memcached) Incr(key string, value int64) ([]byte, error) { 160 | cmd := fmt.Sprintf("incr %s %d\r\n", key, value) 161 | err := m.conn.Send(cmd) 162 | if err != nil { 163 | return nil, err 164 | } 165 | resp, err := m.conn.Receive("incr") 166 | return resp.([]byte), err 167 | } 168 | 169 | func (m *Memcached) Decr(key string, value int64) ([]byte, error) { 170 | cmd := fmt.Sprintf("decr %s %d\r\n", key, value) 171 | err := m.conn.Send(cmd) 172 | if err != nil { 173 | return nil, err 174 | } 175 | resp, err := m.conn.Receive("decr") 176 | return resp.([]byte), err 177 | } 178 | 179 | func (m *Memcached) Touch(key string, expTime int) ([]byte, error) { 180 | cmd := fmt.Sprintf("touch %s %d\r\n", key, expTime) 181 | err := m.conn.Send(cmd) 182 | if err != nil { 183 | return nil, err 184 | } 185 | resp, err := m.conn.Receive("touch") 186 | return resp.([]byte), err 187 | } 188 | 189 | func (m *Memcached) Stats(args ...string) (map[string]string, error) { 190 | var cmd string 191 | if len(args) == 0 { 192 | cmd = "stats\r\n" 193 | } else { 194 | cmd = fmt.Sprintf("stats %s\r\n", args[0]) 195 | } 196 | err := m.conn.Send(cmd) 197 | if err != nil { 198 | return nil, err 199 | } 200 | resp, err := m.conn.Receive("stats") 201 | return resp.(map[string]string), err 202 | } 203 | 204 | // 关闭网络连接 205 | 206 | func (m *Memcached) Close() { 207 | err := m.conn.Conn.Close() 208 | if err != nil { 209 | fmt.Println(err) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /memcached/connection.go: -------------------------------------------------------------------------------- 1 | package memcached 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | ) 11 | 12 | type Connection struct { 13 | Host string 14 | Port int 15 | Conn *net.TCPConn 16 | } 17 | 18 | func (c *Connection) checkError(resp []byte) error { 19 | if bytes.Compare(resp, []byte("ERROR")) == 0 { 20 | return errors.New("发生错误:ERROR") 21 | } 22 | if bytes.HasPrefix(resp, []byte("CLIENT_ERROR ")) { 23 | return errors.New(fmt.Sprintf("发生错误:%s", string(resp))) 24 | } 25 | if bytes.HasPrefix(resp, []byte("SERVER_ERROR ")) { 26 | return errors.New(fmt.Sprintf("发生错误:%s", string(resp))) 27 | } 28 | return nil 29 | } 30 | 31 | func (c *Connection) Open() error { 32 | targetTCPAddress, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", c.Host, c.Port)) 33 | if err != nil { 34 | return err 35 | } 36 | conn, err := net.DialTCP("tcp", nil, targetTCPAddress) 37 | if err != nil { 38 | return err 39 | } 40 | err = conn.SetKeepAlive(true) 41 | if err != nil { 42 | return err 43 | } 44 | c.Conn = conn 45 | return nil 46 | } 47 | 48 | func (c *Connection) Send(cmd ...string) (err error) { 49 | // 写 50 | for _, cmdPart := range cmd { 51 | cmdPartBytes := []byte(cmdPart) 52 | cmdPartLength := len(cmdPartBytes) 53 | allWritenLength := 0 54 | for { 55 | if allWritenLength < cmdPartLength { 56 | writenLength, err := c.Conn.Write(cmdPartBytes[allWritenLength:]) 57 | if err != nil { 58 | return err 59 | } 60 | allWritenLength += writenLength 61 | continue 62 | } 63 | break 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | func (c *Connection) Receive(cmd string) (interface{}, error) { 70 | byteReader := bufio.NewReader(c.Conn) 71 | switch { 72 | case cmd == "get" || cmd == "gets": 73 | mapper := make(map[string]string) 74 | for { 75 | line, err := byteReader.ReadBytes('\n') 76 | if err != nil { 77 | return mapper, err 78 | } 79 | line = bytes.Trim(line, "\r\n") 80 | if string(line) == "END" { 81 | return mapper, nil 82 | } 83 | lineParts := bytes.Split(line, []byte(" ")) 84 | if len(lineParts) != 4 || string(lineParts[0]) != "VALUE" { 85 | err = c.checkError(line) 86 | if err != nil { 87 | return mapper, err 88 | } 89 | return mapper, errors.New("响应数据格式非法") 90 | } 91 | valueLength, err := strconv.Atoi(string(lineParts[3])) 92 | if err != nil { 93 | return mapper, err 94 | } 95 | // 加上\r\n 96 | valueLength += 2 97 | value := make([]byte, valueLength) 98 | bytePosition := 0 99 | for bytePosition < valueLength { 100 | oneByte, err := byteReader.ReadByte() 101 | if err != nil { 102 | return mapper, err 103 | } 104 | value[bytePosition] = oneByte 105 | bytePosition += 1 106 | } 107 | mapper[string(lineParts[1])] = string(bytes.Trim(value, "\r\n")) 108 | } 109 | return mapper, nil 110 | case cmd == "stats": 111 | mapper := make(map[string]string) 112 | for { 113 | line, err := byteReader.ReadBytes('\n') 114 | if err != nil { 115 | return mapper, err 116 | } 117 | line = bytes.Trim(line, "\r\n") 118 | if string(line) == "END" { 119 | return mapper, nil 120 | } 121 | lineParts := bytes.Split(line, []byte(" ")) 122 | if len(lineParts) != 3 || string(lineParts[0]) != "STAT" { 123 | err = c.checkError(line) 124 | if err != nil { 125 | return mapper, err 126 | } 127 | return mapper, errors.New("响应数据格式非法") 128 | } 129 | mapper[string(lineParts[1])] = string(lineParts[2]) 130 | } 131 | return mapper, nil 132 | default: 133 | line, err := byteReader.ReadBytes('\n') 134 | if err != nil { 135 | return line, err 136 | } 137 | line = bytes.Trim(line, "\r\n") 138 | err = c.checkError(line) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return line, nil 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /middleman/manager/manager.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | type MiddlemanInterface interface { 4 | Config(map[string]string) bool 5 | GenInnerKey(string) string 6 | SerializeValue(string) string 7 | UnserializeValue(string) interface{} 8 | } 9 | 10 | var Middlemen = make(map[string]MiddlemanInterface) 11 | 12 | func MiddlemanRegister(id string, thisMiddleman MiddlemanInterface) bool { 13 | if _, ok := Middlemen[id]; ok == true { 14 | return false 15 | } 16 | Middlemen[id] = thisMiddleman 17 | return true 18 | } 19 | 20 | func Get(id string, config map[string]string) MiddlemanInterface { 21 | targetMiddleman, ok := Middlemen[id] 22 | if ok == false { 23 | return nil 24 | } 25 | if targetMiddleman.Config(config) == false { 26 | return nil 27 | } 28 | return targetMiddleman 29 | } 30 | 31 | func init() { 32 | 33 | } 34 | -------------------------------------------------------------------------------- /middleman/middleman/default.go: -------------------------------------------------------------------------------- 1 | package middleman 2 | 3 | import ( 4 | "github.com/youngsterxyf/memcached-ui/middleman/manager" 5 | ) 6 | 7 | type DefaultMiddleman struct{} 8 | 9 | func (dmm *DefaultMiddleman) Config(config map[string]string) bool { 10 | return true 11 | } 12 | 13 | func (dmm DefaultMiddleman) GenInnerKey(key string) string { 14 | return key 15 | } 16 | 17 | func (dmm DefaultMiddleman) SerializeValue(value string) string { 18 | return value 19 | } 20 | 21 | func (dmm DefaultMiddleman) UnserializeValue(value string) interface{} { 22 | return value 23 | } 24 | 25 | func init() { 26 | defaultMiddleman := new(DefaultMiddleman) 27 | manager.MiddlemanRegister("default", defaultMiddleman) 28 | } 29 | -------------------------------------------------------------------------------- /middleman/middleman/unserialize_to_json.php: -------------------------------------------------------------------------------- 1 |
'+JSON.stringify(data, null, 2)+'
'; 15 | } 16 | } else { 17 | toShow = $resp.msg; 18 | } 19 | $("#action_result").html('
' + toShow + '
'); 20 | } 21 | 22 | var targetInstance = $("#target_instance").val(); 23 | 24 | var actionGetVM = new Vue({ 25 | el: "#action_get", 26 | data: { 27 | k: "" 28 | }, 29 | methods: { 30 | getIt: function($e) { 31 | $e.preventDefault(); 32 | var myself = $e.targetVM; 33 | if (myself.k === "") { 34 | return; 35 | } 36 | var req = $.ajax({ 37 | type: 'post', 38 | url: '/do', 39 | data: { 40 | action: "get", 41 | instance: targetInstance, 42 | key: myself.k 43 | }, 44 | dataType: 'json' 45 | }); 46 | req.done(showResp); 47 | } 48 | } 49 | }); 50 | 51 | var actionSetVM = new Vue({ 52 | el: "#action_set", 53 | data: { 54 | k: "", 55 | v: "", 56 | expTime: 0 57 | }, 58 | methods: { 59 | setIt: function($e) { 60 | $e.preventDefault(); 61 | var myself = $e.targetVM; 62 | var req = $.ajax({ 63 | type: 'post', 64 | url: '/do', 65 | data: { 66 | action: "set", 67 | instance: targetInstance, 68 | key: myself.k, 69 | value: myself.v, 70 | exp_time: myself.expTime 71 | }, 72 | dataType: 'json' 73 | }); 74 | req.done(showResp); 75 | } 76 | } 77 | }); 78 | 79 | var actionDeleteVM = new Vue({ 80 | el: "#action_delete", 81 | data: { 82 | k: "" 83 | }, 84 | methods: { 85 | deleteIt: function($e) { 86 | $e.preventDefault(); 87 | var myself = $e.targetVM; 88 | var req = $.ajax({ 89 | type: 'post', 90 | url: '/do', 91 | data: { 92 | action: "delete", 93 | instance: targetInstance, 94 | key: myself.k 95 | }, 96 | dataType: 'json' 97 | }); 98 | req.done(showResp); 99 | } 100 | } 101 | }); 102 | 103 | var actionFlushAllVM = new Vue({ 104 | el: "#action_flushall", 105 | data: { 106 | }, 107 | methods: { 108 | flushIt: function($e) { 109 | $e.preventDefault(); 110 | var myself = $e.targetVM; 111 | var req = $.ajax({ 112 | type: 'post', 113 | url: '/do', 114 | data: { 115 | action: "flush_all", 116 | instance: targetInstance, 117 | }, 118 | dataType: 'json' 119 | }); 120 | req.done(showResp); 121 | } 122 | } 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /ui/templates/cluster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Memcached UI 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 |
21 | 46 | {{range $err := .InfoErrs}} 47 | 48 | {{end}} 49 |
50 | {{range $stat := .StatsInfos}} 51 |
52 |
53 |
54 | {{$stat.InstanceID}} 概况 55 |
56 |
57 |
58 |
:{{$stat.Source}}
59 |
:{{$stat.Pid}} (v{{$stat.Version}})
60 |
:{{$stat.CurrConnections}}
61 |
:{{$stat.Uptime}}
62 |
:{{$stat.CurrMemoryUsage}} / {{$stat.MaxMemoryLimit}}
63 |
:{{$stat.CurrItems}}
64 |
:{{$stat.GetHits}}
65 |
:{{$stat.GetMisses}}
66 |
:{{$stat.GetRate}}%
67 |
68 |
69 |
70 |
71 | {{end}} 72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ui/templates/node.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Memcached UI 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 |
21 | 46 | {{if .HasInfoErr }} 47 | 48 | {{end}} 49 |
50 |
概况
51 |
52 |
53 |
54 |
:{{.StatsInfo.Source}}
55 |
:{{.StatsInfo.Version}}
56 |
57 |
58 |
:{{.StatsInfo.Pid}}
59 |
:{{.StatsInfo.Uptime}}
60 |
61 |
62 |
:{{.StatsInfo.MaxMemoryLimit}}
63 |
:{{.StatsInfo.CurrMemoryUsage}}
64 |
65 |
66 |
:{{.StatsInfo.CurrItems}}
67 |
:{{.StatsInfo.CurrConnections}}
68 |
69 |
70 |
:{{.StatsInfo.GetHits}}
71 |
:{{.StatsInfo.GetMisses}}
72 |
73 |
74 |
:{{.StatsInfo.GetRate}}%
75 |
76 |
77 |
78 |
79 |
80 |
81 |
操作
82 |
83 | 84 | 85 | 91 | 92 |
93 |
94 |
95 |
96 | 97 |
98 | 99 |
100 |
101 |
102 |
103 |
104 | 105 | 106 | 107 |
108 | 109 |
110 |
111 |
112 |
113 |
114 | 115 |
116 | 117 |
118 |
119 |
120 | 121 |
122 |
123 |
124 |
125 |
126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 | --------------------------------------------------------------------------------