├── .gitignore ├── COPYING ├── Makefile ├── README.md ├── benchmark ├── Makefile ├── benchmark.go ├── benchmark_connection.go ├── benchmark_group.go ├── benchmark_login.go ├── benchmark_room.go ├── benchmark_route.go ├── benchmark_sender.go └── benchmark_storage.go ├── db.sql ├── doc ├── api.md └── design.md ├── go.mod ├── go.sum ├── handler ├── http_handler.go └── logging_handler.go ├── im.cfg.sample ├── im ├── Makefile ├── auth.go ├── config.go ├── http_server.go ├── im_test.go ├── listener.go ├── main.go ├── stat_memory.go └── ws.go ├── im_client.py ├── im_demo.py ├── imr.cfg.sample ├── imr ├── Makefile ├── config.go └── main.go ├── ims.cfg.sample ├── ims ├── Makefile ├── config.go ├── main.go ├── monitoring.go ├── rpc.go └── storage_test.go ├── lru └── lru.go ├── protocol ├── command.go ├── message.go └── protocol.go ├── router ├── app_route.go ├── channel.go ├── client.go ├── route.go ├── route_message.go ├── rpc.go └── server.go ├── server ├── api.go ├── app.go ├── app_route.go ├── client.go ├── connection.go ├── customer_server.go ├── device.go ├── group.go ├── group_loader.go ├── group_manager.go ├── group_message_deliver.go ├── group_message_file.go ├── group_server.go ├── group_service.go ├── im_message.go ├── message.go ├── monitoring.go ├── peer_server.go ├── push_service.go ├── redis_config.go ├── relationship.go ├── relationship_pool.go ├── room_server.go ├── route.go ├── rpc.go ├── server.go ├── server_summary.go ├── subscriber.go └── user.go ├── set └── set.go ├── storage ├── group_storage.go ├── master.go ├── peer_storage.go ├── rpc.go ├── slaver.go ├── storage.go ├── storage_file.go ├── storage_message.go └── sync_client.go ├── tests ├── client.py ├── config.py ├── protocol.py ├── rpc.py ├── test_customer.py ├── test_group.py ├── test_peer.py └── test_room.py └── tools └── truncate.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | .idea 4 | bin 5 | *.cfg 6 | im/im 7 | ims/ims 8 | imr/imr 9 | benchmark/benchmark 10 | benchmark_group 11 | benchmark_connection 12 | benchmark_sender 13 | main.test 14 | benchmark_storage 15 | benchmark_route 16 | benchmark_room 17 | ims/ims_truncate 18 | *.log 19 | *.pyc 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all:im_bin ims_bin imr_bin 2 | 3 | 4 | im_bin: 5 | cd im && make 6 | 7 | ims_bin: 8 | cd ims && make 9 | 10 | imr_bin: 11 | cd imr && make 12 | 13 | im_truncate: 14 | go build -o ./bin/im_truncate ./tools/ ; 15 | 16 | install:all 17 | cp ./im/im ./bin 18 | cp ./ims/ims ./bin 19 | cp ./imr/imr ./bin 20 | 21 | clean: 22 | rm -f ./im/im ./im/benchmark ./im/benchmark_group ./im/benchmark_room ./im/benchmark_connection ./im/benchmark_sender ./im/benchmark_storage ./im/benchmark_route ./ims/main.test ./ims/ims ./imr/imr 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # im service 3 | 1. 支持点对点消息, 群组消息, 聊天室消息 4 | 2. 支持集群部署 5 | 3. 单机支持50w用户在线 6 | 4. 单机处理消息5000条/s 7 | 5. 支持超大群组(3000人) 8 | 9 | *服务器硬件指标:32g 16核* 10 | 11 | ## 编译运行 12 | 13 | 1. 安装go编译环境 14 | 15 | 版本要求:支持module,高于v1.11 16 | 参考链接:https://golang.org/doc/install 17 | 18 | 2. 下载im_service代码 19 | 20 | git clone https://github.com/GoBelieveIO/im_service.git 21 | 22 | 3. 编译 23 | 24 | cd im_service 25 | 26 | mkdir bin 27 | 28 | make install 29 | 30 | 可执行程序在bin目录下 31 | 32 | 4. 安装mysql数据库, redis, 并导入db.sql 33 | 34 | 5. 配置程序 35 | 配置项的说明参考ims.cfg.sample, imr.cfg.sample, im.cfg.sample 36 | 37 | 6. 启动程序 38 | 39 | * 创建配置文件中配置的im&ims消息存放路径 40 | 41 | mkdir /tmp/im 42 | 43 | mkdir /tmp/impending 44 | 45 | * 创建日志文件路径 46 | 47 | mkdir /data/logs/ims 48 | 49 | mkdir /data/logs/imr 50 | 51 | mkdir /data/logs/im 52 | 53 | * 启动im服务 54 | 55 | pushd \`dirname $0\` > /dev/null 56 | 57 | BASEDIR=\`pwd\` 58 | 59 | nohup $BASEDIR/ims ims.cfg >/data/logs/ims/ims.err 2>&1 & 60 | 61 | nohup $BASEDIR/imr imr.cfg >/data/logs/imr/imr.err 2>&1 & 62 | 63 | nohup $BASEDIR/im im.cfg >/data/logs/im/im.err 2>&1 & 64 | 65 | 66 | ## token的格式 67 | 68 | 连接im服务器token存储在redis的hash对象中,脱离API服务器测试时,可以手工生成。 69 | $token就是客户端需要获得的, 用来连接im服务器的认证信息。 70 | key:access_token_$token 71 | field: 72 | user_id:用户id 73 | app_id:应用id 74 | 75 | 76 | ## 官方QQ群 77 | 1. 450359487(一群),已满。 78 | 2. 416969931(二群),加群请附加说明信息。 79 | 80 | ## 官方网站 81 | https://developer.gobelieve.io/ 82 | 83 | ## 相关产品 84 | https://goubuli.mobi/ 85 | -------------------------------------------------------------------------------- /benchmark/Makefile: -------------------------------------------------------------------------------- 1 | all:benchmark benchmark_group benchmark_room benchmark_connection benchmark_sender benchmark_storage benchmark_route 2 | 3 | benchmark:benchmark.go benchmark_login.go protocol.go message.go im_message.go 4 | go build benchmark.go benchmark_login.go protocol.go message.go im_message.go; 5 | 6 | benchmark_group:benchmark_group.go benchmark_login.go protocol.go message.go im_message.go 7 | go build benchmark_group.go benchmark_login.go protocol.go message.go im_message.go; 8 | 9 | benchmark_room:benchmark_room.go benchmark_login.go protocol.go message.go im_message.go 10 | go build benchmark_room.go benchmark_login.go protocol.go message.go im_message.go; 11 | 12 | benchmark_connection:benchmark_connection.go benchmark_login.go protocol.go message.go im_message.go 13 | go build benchmark_connection.go benchmark_login.go protocol.go message.go im_message.go; 14 | 15 | benchmark_sender:benchmark_sender.go benchmark_login.go protocol.go message.go im_message.go 16 | go build benchmark_sender.go benchmark_login.go protocol.go message.go im_message.go; 17 | 18 | benchmark_storage:benchmark_storage.go rpc.go protocol.go message.go im_message.go 19 | go build -o benchmark_storage benchmark_storage.go rpc.go protocol.go message.go im_message.go; 20 | 21 | benchmark_route:benchmark_route.go channel.go protocol.go message.go route_message.go 22 | go build -o benchmark_route benchmark_route.go channel.go protocol.go message.go im_message.go route_message.go; 23 | 24 | clean: 25 | rm -f benchmark benchmark_group benchmark_room benchmark_connection benchmark_sender benchmark_storage benchmark_route 26 | -------------------------------------------------------------------------------- /benchmark/benchmark.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "math/rand" 10 | "net" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/gomodule/redigo/redis" 15 | ) 16 | 17 | const HOST = "127.0.0.1" 18 | const PORT = 23000 19 | const redis_address = "127.0.0.1" 20 | const redis_password = "" 21 | const redis_db = 0 22 | 23 | var redis_pool *redis.Pool 24 | var seededRand *rand.Rand 25 | var concurrent int 26 | var count int 27 | var c chan bool 28 | 29 | func init() { 30 | flag.IntVar(&concurrent, "c", 10, "concurrent number") 31 | flag.IntVar(&count, "n", 100000, "request number") 32 | } 33 | 34 | func send(uid int64, receiver int64, sem chan int) { 35 | ip := net.ParseIP(HOST) 36 | addr := net.TCPAddr{ip, PORT, ""} 37 | 38 | token, err := login(uid) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | conn, err := net.DialTCP("tcp4", nil, &addr) 44 | if err != nil { 45 | log.Println("connect error") 46 | return 47 | } 48 | seq := 1 49 | auth := &AuthenticationToken{token: token, platform_id: 1, device_id: "00000000"} 50 | SendMessage(conn, &Message{cmd: MSG_AUTH_TOKEN, seq: seq, version: DEFAULT_VERSION, body: auth}) 51 | ReceiveMessage(conn) 52 | 53 | send_count := 0 54 | for i := 0; i < count; i++ { 55 | content := fmt.Sprintf("test....%d", i) 56 | seq++ 57 | msg := &Message{cmd: MSG_IM, seq: seq, version: DEFAULT_VERSION, flag: 0, 58 | body: &IMMessage{uid, receiver, 0, int32(i), content}} 59 | 60 | select { 61 | case <-sem: 62 | break 63 | case <-time.After(1 * time.Second): 64 | log.Println("wait send sem timeout") 65 | } 66 | 67 | SendMessage(conn, msg) 68 | 69 | var ack *Message 70 | for { 71 | mm := ReceiveMessage(conn) 72 | if mm == nil { 73 | 74 | break 75 | } 76 | if mm.cmd == MSG_ACK { 77 | ack = mm 78 | break 79 | } 80 | } 81 | 82 | if ack != nil { 83 | send_count++ 84 | } else { 85 | log.Println("recv ack error") 86 | break 87 | } 88 | } 89 | conn.Close() 90 | c <- true 91 | log.Printf("%d send complete:%d", uid, send_count) 92 | } 93 | 94 | func receive(uid int64, limit int, sem chan int) { 95 | sync_key := int64(0) 96 | 97 | ip := net.ParseIP(HOST) 98 | addr := net.TCPAddr{ip, PORT, ""} 99 | 100 | token, err := login(uid) 101 | 102 | if err != nil { 103 | panic(err) 104 | } 105 | 106 | conn, err := net.DialTCP("tcp4", nil, &addr) 107 | if err != nil { 108 | log.Println("connect error") 109 | return 110 | } 111 | seq := 1 112 | auth := &AuthenticationToken{token: token, platform_id: 1, device_id: "00000000"} 113 | SendMessage(conn, &Message{cmd: MSG_AUTH_TOKEN, seq: seq, version: DEFAULT_VERSION, flag: 0, body: auth}) 114 | ReceiveMessage(conn) 115 | 116 | seq++ 117 | ss := &Message{cmd: MSG_SYNC, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &SyncKey{sync_key}} 118 | SendMessage(conn, ss) 119 | 120 | //一次同步的取到的消息数目 121 | sync_count := 0 122 | 123 | recv_count := 0 124 | syncing := false 125 | pending_sync := false 126 | for { 127 | if limit > 0 { 128 | conn.SetDeadline(time.Now().Add(40 * time.Second)) 129 | } else { 130 | conn.SetDeadline(time.Now().Add(400 * time.Second)) 131 | } 132 | 133 | msg := ReceiveMessage(conn) 134 | if msg == nil { 135 | log.Println("receive nill message") 136 | break 137 | } 138 | 139 | if msg.cmd == MSG_SYNC_NOTIFY { 140 | if !syncing { 141 | seq++ 142 | s := &Message{cmd: MSG_SYNC, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &SyncKey{sync_key}} 143 | SendMessage(conn, s) 144 | syncing = true 145 | } else { 146 | pending_sync = true 147 | } 148 | } else if msg.cmd == MSG_IM { 149 | //m := msg.body.(*IMMessage) 150 | //log.Printf("sender:%d receiver:%d content:%s", m.sender, m.receiver, m.content) 151 | 152 | recv_count += 1 153 | if limit > 0 && recv_count <= limit { 154 | select { 155 | case sem <- 1: 156 | break 157 | case <-time.After(10 * time.Millisecond): 158 | log.Println("increment timeout") 159 | } 160 | } 161 | 162 | sync_count++ 163 | 164 | seq++ 165 | ack := &Message{cmd: MSG_ACK, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &MessageACK{seq: int32(msg.seq)}} 166 | SendMessage(conn, ack) 167 | } else if msg.cmd == MSG_SYNC_BEGIN { 168 | sync_count = 0 169 | //log.Println("sync begin:", recv_count) 170 | } else if msg.cmd == MSG_SYNC_END { 171 | syncing = false 172 | s := msg.body.(*SyncKey) 173 | //log.Println("sync end:", recv_count, s.sync_key, sync_key) 174 | if s.sync_key > sync_key { 175 | sync_key = s.sync_key 176 | //log.Println("sync key:", sync_key) 177 | seq++ 178 | sk := &Message{cmd: MSG_SYNC_KEY, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &SyncKey{sync_key}} 179 | SendMessage(conn, sk) 180 | } 181 | 182 | if limit < 0 && sync_count == 0 { 183 | break 184 | } 185 | 186 | if limit > 0 && recv_count >= limit { 187 | break 188 | } 189 | 190 | if pending_sync { 191 | seq++ 192 | s := &Message{cmd: MSG_SYNC, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &SyncKey{sync_key}} 193 | SendMessage(conn, s) 194 | syncing = true 195 | pending_sync = false 196 | } 197 | 198 | } else { 199 | log.Println("mmmmmm:", Command(msg.cmd)) 200 | } 201 | } 202 | conn.Close() 203 | c <- true 204 | 205 | log.Printf("%d received:%d", uid, recv_count) 206 | } 207 | 208 | func main() { 209 | runtime.GOMAXPROCS(4) 210 | flag.Parse() 211 | 212 | fmt.Printf("concurrent:%d, request:%d\n", concurrent, count) 213 | 214 | log.SetFlags(log.Lshortfile | log.LstdFlags) 215 | 216 | seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) 217 | redis_pool = NewRedisPool(redis_address, redis_password, redis_db) 218 | 219 | c = make(chan bool, 100) 220 | u := int64(13635273140) 221 | 222 | sems := make([]chan int, concurrent) 223 | 224 | for i := 0; i < concurrent; i++ { 225 | sems[i] = make(chan int, 2000) 226 | for j := 0; j < 1000; j++ { 227 | sems[i] <- 1 228 | } 229 | } 230 | 231 | //接受历史离线消息 232 | for i := 0; i < concurrent; i++ { 233 | go receive(u+int64(concurrent+i), -1, sems[i]) 234 | } 235 | 236 | for i := 0; i < concurrent; i++ { 237 | <-c 238 | } 239 | 240 | time.Sleep(1 * time.Second) 241 | 242 | //启动接受者 243 | for i := 0; i < concurrent; i++ { 244 | go receive(u+int64(concurrent+i), count, sems[i]) 245 | } 246 | 247 | time.Sleep(2 * time.Second) 248 | 249 | begin := time.Now().UnixNano() 250 | log.Println("begin test:", begin) 251 | 252 | for i := 0; i < concurrent; i++ { 253 | go send(u+int64(i), u+int64(i+concurrent), sems[i]) 254 | } 255 | for i := 0; i < 2*concurrent; i++ { 256 | <-c 257 | } 258 | 259 | end := time.Now().UnixNano() 260 | 261 | var tps int64 = 0 262 | if end-begin > 0 { 263 | tps = int64(1000*1000*1000*concurrent*count) / (end - begin) 264 | } 265 | fmt.Println("tps:", tps) 266 | } 267 | -------------------------------------------------------------------------------- /benchmark/benchmark_connection.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "runtime" 11 | "time" 12 | 13 | "github.com/gomodule/redigo/redis" 14 | ) 15 | 16 | const APPID = 7 17 | const REDIS_HOST = "127.0.0.1:6379" 18 | const REDIS_PASSWORD = "" 19 | const REDIS_DB = 0 20 | 21 | var first int64 22 | var last int64 23 | var local_ip string 24 | var host string 25 | var port int 26 | 27 | var seededRand *rand.Rand 28 | var redis_pool *redis.Pool 29 | 30 | func init() { 31 | flag.Int64Var(&first, "first", 0, "first uid") 32 | flag.Int64Var(&last, "last", 0, "last uid") 33 | flag.StringVar(&local_ip, "local_ip", "0.0.0.0", "local ip") 34 | flag.StringVar(&host, "host", "127.0.0.1", "host") 35 | flag.IntVar(&port, "port", 23000, "port") 36 | } 37 | 38 | func receive(uid int64) { 39 | ip := net.ParseIP(host) 40 | addr := net.TCPAddr{ip, port, ""} 41 | 42 | lip := net.ParseIP(local_ip) 43 | laddr := net.TCPAddr{lip, 0, ""} 44 | 45 | conn, err := net.DialTCP("tcp4", &laddr, &addr) 46 | if err != nil { 47 | log.Println("connect error") 48 | 49 | return 50 | } 51 | 52 | token, err := login(uid) 53 | 54 | seq := 1 55 | SendMessage(conn, &Message{cmd: MSG_AUTH_TOKEN, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &AuthenticationToken{token: token, platform_id: PLATFORM_WEB, device_id: "1"}}) 56 | ReceiveMessage(conn) 57 | 58 | q := make(chan bool, 10) 59 | wt := make(chan *Message, 10) 60 | 61 | const HEARTBEAT_TIMEOUT = 3 * 60 62 | go func() { 63 | msgid := 0 64 | ticker := time.NewTicker(HEARTBEAT_TIMEOUT * time.Second) 65 | ticker2 := time.NewTicker(150 * time.Second) 66 | for { 67 | select { 68 | case <-ticker2.C: 69 | seq++ 70 | msgid++ 71 | receiver := first + rand.Int63()%(last-first) 72 | im := &IMMessage{uid, receiver, 0, int32(msgid), "test"} 73 | SendMessage(conn, &Message{cmd: MSG_IM, seq: seq, version: DEFAULT_VERSION, flag: 0, body: im}) 74 | case <-ticker.C: 75 | seq++ 76 | SendMessage(conn, &Message{cmd: MSG_PING, seq: seq, version: DEFAULT_VERSION, flag: 0, body: nil}) 77 | case m := <-wt: 78 | if m == nil { 79 | q <- true 80 | return 81 | } 82 | seq++ 83 | m.seq = seq 84 | SendMessage(conn, m) 85 | } 86 | } 87 | }() 88 | 89 | go func() { 90 | for { 91 | msg := ReceiveMessage(conn) 92 | if msg == nil { 93 | wt <- nil 94 | q <- true 95 | return 96 | } 97 | 98 | if msg.cmd == MSG_IM || msg.cmd == MSG_GROUP_IM { 99 | ack := &Message{cmd: MSG_ACK, body: &MessageACK{seq: int32(msg.seq)}} 100 | wt <- ack 101 | } 102 | } 103 | }() 104 | 105 | <-q 106 | <-q 107 | conn.Close() 108 | } 109 | 110 | func receive_loop(uid int64) { 111 | for { 112 | receive(uid) 113 | n := rand.Int() 114 | n = n % 20 115 | time.Sleep(time.Duration(n) * time.Second) 116 | } 117 | } 118 | 119 | func main() { 120 | runtime.GOMAXPROCS(4) 121 | rand.Seed(time.Now().Unix()) 122 | 123 | flag.Parse() 124 | log.Printf("first:%d last:%d local ip:%s host:%s port:%d\n", 125 | first, last, local_ip, host, port) 126 | 127 | log.SetFlags(log.Lshortfile | log.LstdFlags) 128 | 129 | seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) 130 | redis_pool = NewRedisPool(REDIS_HOST, REDIS_PASSWORD, REDIS_DB) 131 | 132 | c := make(chan bool, 100) 133 | var i int64 134 | var j int64 135 | 136 | for i = first; i < last; i += 1000 { 137 | for j = i; j < i+1000 && j < last; j++ { 138 | go receive_loop(j) 139 | } 140 | time.Sleep(2 * time.Second) 141 | } 142 | 143 | for i = first; i < last; i++ { 144 | <-c 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /benchmark/benchmark_group.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "math/rand" 10 | "net" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/gomodule/redigo/redis" 15 | ) 16 | 17 | const HOST = "127.0.0.1" 18 | const PORT = 23000 19 | 20 | var concurrent int 21 | var count int 22 | var recv_count int 23 | var c chan bool 24 | var redis_pool *redis.Pool 25 | var seededRand *rand.Rand 26 | 27 | const redis_address = "127.0.0.1:6379" 28 | const redis_password = "" 29 | const redis_db = 0 30 | 31 | func init() { 32 | flag.IntVar(&concurrent, "c", 10, "concurrent number") 33 | flag.IntVar(&recv_count, "r", 20, "recv number") 34 | flag.IntVar(&count, "n", 5000, "request number") 35 | } 36 | 37 | func recv(uid int64, gid int64, conn *net.TCPConn) { 38 | seq := 1 39 | 40 | n := count * (concurrent) 41 | total := n 42 | for i := 0; i < n; i++ { 43 | conn.SetDeadline(time.Now().Add(40 * time.Second)) 44 | msg := ReceiveMessage(conn) 45 | if msg == nil { 46 | log.Println("receive nill message") 47 | total = i 48 | break 49 | } 50 | 51 | if msg.cmd != MSG_GROUP_IM { 52 | log.Println("mmmmmm:", Command(msg.cmd)) 53 | i-- 54 | } 55 | 56 | if msg.cmd == MSG_GROUP_IM { 57 | //m := msg.body.(*IMMessage) 58 | //log.Printf("sender:%d receiver:%d content:%s", m.sender, m.receiver, m.content) 59 | } 60 | seq++ 61 | ack := &Message{cmd: MSG_ACK, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &MessageACK{seq: int32(msg.seq)}} 62 | SendMessage(conn, ack) 63 | } 64 | log.Printf("%d received:%d", uid, total) 65 | c <- true 66 | } 67 | 68 | func send(uid int64, gid int64, conn *net.TCPConn) { 69 | ack_c := make(chan int, 100) 70 | close_c := make(chan bool) 71 | 72 | seq := 1 73 | 74 | go func() { 75 | c := count * (concurrent - 1) 76 | total := c 77 | for i := 0; i < c; i++ { 78 | conn.SetDeadline(time.Now().Add(40 * time.Second)) 79 | msg := ReceiveMessage(conn) 80 | if msg == nil { 81 | log.Println("receive nill message") 82 | total = i 83 | break 84 | } 85 | 86 | if msg.cmd == MSG_ACK { 87 | i-- 88 | ack_c <- 0 89 | continue 90 | } 91 | 92 | if msg.cmd != MSG_GROUP_IM { 93 | log.Println("mmmmmm:", Command(msg.cmd)) 94 | i-- 95 | } 96 | 97 | if msg.cmd == MSG_GROUP_IM { 98 | //m := msg.body.(*IMMessage) 99 | //log.Printf("sender:%d receiver:%d content:%s", m.sender, m.receiver, m.content) 100 | } 101 | seq++ 102 | ack := &Message{cmd: MSG_ACK, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &MessageACK{seq: int32(msg.seq)}} 103 | SendMessage(conn, ack) 104 | } 105 | log.Printf("%d received:%d", uid, total) 106 | close(close_c) 107 | }() 108 | 109 | for i := 0; i < count; i++ { 110 | content := fmt.Sprintf("test....%d", i) 111 | seq++ 112 | msg := &Message{cmd: MSG_GROUP_IM, seq: seq, version: DEFAULT_VERSION, flag: 0, 113 | body: &IMMessage{uid, gid, 0, int32(i), content}} 114 | SendMessage(conn, msg) 115 | var e bool 116 | select { 117 | case <-ack_c: 118 | case <-close_c: 119 | for { 120 | ack := ReceiveMessage(conn) 121 | if ack == nil { 122 | e = true 123 | break 124 | } 125 | if ack.cmd == MSG_ACK { 126 | break 127 | } 128 | } 129 | } 130 | if e { 131 | break 132 | } 133 | } 134 | 135 | <-close_c 136 | 137 | conn.Close() 138 | log.Printf("%d send complete", uid) 139 | c <- true 140 | } 141 | 142 | func ConnectServer(uid int64) *net.TCPConn { 143 | var token string 144 | for i := 0; i < 2; i++ { 145 | var err error 146 | token, err = login(uid) 147 | if err != nil { 148 | log.Println("login err:", err) 149 | continue 150 | } 151 | } 152 | 153 | if token == "" { 154 | panic("") 155 | } 156 | log.Println("login success:", token) 157 | 158 | ip := net.ParseIP(HOST) 159 | addr := net.TCPAddr{ip, PORT, ""} 160 | conn, err := net.DialTCP("tcp4", nil, &addr) 161 | if err != nil { 162 | log.Println("connect error:", err) 163 | return nil 164 | } 165 | seq := 1 166 | auth := &AuthenticationToken{token: token, platform_id: 1, device_id: "00000000"} 167 | SendMessage(conn, &Message{cmd: MSG_AUTH_TOKEN, seq: seq, version: DEFAULT_VERSION, flag: 0, body: auth}) 168 | ReceiveMessage(conn) 169 | 170 | log.Printf("uid:%d connected\n", uid) 171 | return conn 172 | 173 | } 174 | 175 | func main() { 176 | runtime.GOMAXPROCS(4) 177 | flag.Parse() 178 | 179 | log.SetFlags(log.Lshortfile | log.LstdFlags) 180 | 181 | log.Printf("concurrent:%d, recv:%d, request:%d\n", concurrent, recv_count, count) 182 | seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) 183 | redis_pool = NewRedisPool(redis_address, redis_password, redis_db) 184 | 185 | c = make(chan bool, 100) 186 | u := int64(13635273140) 187 | 188 | //171(5000) 173(2000) 189 | gid := int64(171) 190 | 191 | //test_send(u, gid) 192 | //return 193 | 194 | conns := make([]*net.TCPConn, 0, 1000) 195 | 196 | for i := 0; i < concurrent+recv_count; i++ { 197 | 198 | conn := ConnectServer(u + int64(i)) 199 | conns = append(conns, conn) 200 | if i%100 == 0 && i > 0 { 201 | time.Sleep(time.Second * 1) 202 | } 203 | } 204 | time.Sleep(time.Second * 1) 205 | 206 | fmt.Println("connected") 207 | 208 | begin := time.Now().UnixNano() 209 | for i := 0; i < concurrent; i++ { 210 | go send(u+int64(i), gid, conns[i]) 211 | } 212 | for i := 0; i < recv_count; i++ { 213 | go recv(u+int64(concurrent+i), gid, conns[i+concurrent]) 214 | } 215 | 216 | for i := 0; i < concurrent+recv_count; i++ { 217 | <-c 218 | } 219 | 220 | end := time.Now().UnixNano() 221 | 222 | var tps int64 = 0 223 | if end-begin > 0 { 224 | tps = int64(1000*1000*1000*concurrent*count) / (end - begin) 225 | } 226 | fmt.Println("tps:", tps) 227 | } 228 | -------------------------------------------------------------------------------- /benchmark/benchmark_login.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | 9 | "github.com/gomodule/redigo/redis" 10 | ) 11 | 12 | const APP_ID = 7 13 | const CHARSET = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 14 | 15 | func RandomStringWithCharset(length int, charset string) string { 16 | b := make([]byte, length) 17 | for i := range b { 18 | b[i] = charset[seededRand.Intn(len(charset))] 19 | } 20 | return string(b) 21 | } 22 | 23 | func NewRedisPool(server, password string, db int) *redis.Pool { 24 | return &redis.Pool{ 25 | MaxIdle: 100, 26 | MaxActive: 500, 27 | IdleTimeout: 480 * time.Second, 28 | Dial: func() (redis.Conn, error) { 29 | timeout := time.Duration(2) * time.Second 30 | c, err := redis.DialTimeout("tcp", server, timeout, 0, 0) 31 | if err != nil { 32 | return nil, err 33 | } 34 | if len(password) > 0 { 35 | if _, err := c.Do("AUTH", password); err != nil { 36 | c.Close() 37 | return nil, err 38 | } 39 | } 40 | if db > 0 && db < 16 { 41 | if _, err := c.Do("SELECT", db); err != nil { 42 | c.Close() 43 | return nil, err 44 | } 45 | } 46 | return c, err 47 | }, 48 | } 49 | } 50 | 51 | func login(uid int64) (string, error) { 52 | conn := redis_pool.Get() 53 | defer conn.Close() 54 | 55 | token := RandomStringWithCharset(24, CHARSET) 56 | 57 | key := fmt.Sprintf("access_token_%s", token) 58 | _, err := conn.Do("HMSET", key, "access_token", token, "user_id", uid, "app_id", APP_ID) 59 | if err != nil { 60 | return "", err 61 | } 62 | _, err = conn.Do("PEXPIRE", key, 1000*3600*4) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | return token, nil 68 | } 69 | -------------------------------------------------------------------------------- /benchmark/benchmark_room.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "math/rand" 10 | "net" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/gomodule/redigo/redis" 15 | ) 16 | 17 | const HOST = "127.0.0.1" 18 | const PORT = 23000 19 | const redis_address = "192.168.33.10:6379" 20 | const redis_password = "123456" 21 | const redis_db = 1 22 | 23 | type Param struct { 24 | concurrent int 25 | count int 26 | recv_count int 27 | room_id int 28 | wait int 29 | } 30 | 31 | var param Param 32 | 33 | var send_c chan bool 34 | var recv_c chan bool 35 | 36 | var redis_pool *redis.Pool 37 | var seededRand *rand.Rand 38 | 39 | func init() { 40 | flag.IntVar(¶m.concurrent, "c", 10, "concurrent number") 41 | flag.IntVar(¶m.recv_count, "r", 20, "recv number") 42 | flag.IntVar(¶m.count, "n", 5000, "request number") 43 | flag.IntVar(¶m.room_id, "i", 10, "room id") 44 | flag.IntVar(¶m.wait, "w", 0, "wait before send") 45 | } 46 | 47 | func recv(uid int64, room_id int64, conn *net.TCPConn, count int, concurrent int) { 48 | seq := 2 49 | 50 | pingTs := time.Now() 51 | n := count * (concurrent) 52 | received_num := 0 53 | 54 | for received_num < n { 55 | now := time.Now() 56 | d := now.Sub(pingTs) 57 | if d > time.Duration(3*60)*time.Second { 58 | seq++ 59 | p := &Message{cmd: MSG_PING, seq: seq, version: DEFAULT_VERSION, flag: 0} 60 | err := SendMessage(conn, p) 61 | if err != nil { 62 | log.Println("send ping err:", err) 63 | } 64 | pingTs = now 65 | } 66 | 67 | conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 68 | msg, err := ReceiveServerMessage(conn) 69 | if err != nil { 70 | if nerr, ok := err.(net.Error); ok && nerr.Timeout() { 71 | log.Println("receive message timeout err:", err) 72 | continue 73 | } 74 | log.Println("receive message err:", err) 75 | break 76 | } 77 | 78 | if msg.cmd == MSG_ROOM_IM { 79 | received_num++ 80 | if received_num%10000 == 0 { 81 | log.Printf("%d received:%d", uid, received_num) 82 | } 83 | } 84 | } 85 | log.Printf("%d received:%d", uid, received_num) 86 | recv_c <- true 87 | } 88 | 89 | func send(uid int64, room_id int64, conn *net.TCPConn, count int, concurrent int) { 90 | ack_c := make(chan int, 100) 91 | close_c := make(chan bool) 92 | 93 | seq := 2 94 | 95 | go func() { 96 | n := count * (concurrent - 1) 97 | received_num := 0 98 | ack_num := 0 99 | for received_num < n || ack_num < count { 100 | conn.SetDeadline(time.Now().Add(90 * time.Second)) 101 | msg := ReceiveMessage(conn) 102 | if msg == nil { 103 | log.Println("receive nil message") 104 | break 105 | } 106 | if msg.cmd == MSG_ACK { 107 | ack_num++ 108 | if ack_num%1000 == 0 { 109 | log.Printf("%d ack:%d", uid, ack_num) 110 | } 111 | ack_c <- 0 112 | continue 113 | } 114 | 115 | if msg.cmd == MSG_ROOM_IM { 116 | received_num++ 117 | if received_num%10000 == 0 { 118 | log.Printf("%d received:%d", uid, received_num) 119 | } 120 | } 121 | } 122 | log.Printf("%d ack:%d received:%d", uid, ack_num, received_num) 123 | close(close_c) 124 | }() 125 | 126 | for i := 0; i < count; i++ { 127 | begin := time.Now() 128 | content := fmt.Sprintf("test....%d", i) 129 | seq++ 130 | msg := &Message{cmd: MSG_ROOM_IM, seq: seq, version: DEFAULT_VERSION, flag: 0, 131 | body: &RoomMessage{&RTMessage{uid, room_id, content}}} 132 | SendMessage(conn, msg) 133 | 134 | var e bool 135 | select { 136 | case <-ack_c: 137 | break 138 | case <-close_c: 139 | e = true 140 | break 141 | } 142 | if e { 143 | break 144 | } 145 | 146 | end := time.Now() 147 | d := end.Sub(begin) 148 | if i%100 == 0 { 149 | log.Printf("send message duration:%d", d) 150 | } 151 | if param.wait != 0 { 152 | if d >= time.Duration(param.wait)*time.Second { 153 | continue 154 | } else { 155 | time.Sleep(time.Duration(param.wait)*time.Second - d) 156 | } 157 | } 158 | } 159 | 160 | <-close_c 161 | 162 | conn.Close() 163 | log.Printf("%d send complete", uid) 164 | send_c <- true 165 | } 166 | 167 | func ConnectServer(uid int64) *net.TCPConn { 168 | token, err := login(uid) 169 | if err != nil { 170 | panic(err) 171 | } 172 | log.Println("login success:", token) 173 | 174 | ip := net.ParseIP(HOST) 175 | addr := net.TCPAddr{ip, PORT, ""} 176 | conn, err := net.DialTCP("tcp4", nil, &addr) 177 | if err != nil { 178 | log.Println("connect error:", err) 179 | return nil 180 | } 181 | seq := 1 182 | auth := &AuthenticationToken{token: token, platform_id: 1, device_id: "00000000"} 183 | SendMessage(conn, &Message{cmd: MSG_AUTH_TOKEN, seq: seq, version: DEFAULT_VERSION, flag: 0, body: auth}) 184 | ReceiveMessage(conn) 185 | 186 | log.Printf("uid:%d connected\n", uid) 187 | return conn 188 | 189 | } 190 | 191 | func main() { 192 | runtime.GOMAXPROCS(runtime.NumCPU()) 193 | flag.Parse() 194 | 195 | log.SetFlags(log.Lshortfile | log.LstdFlags) 196 | 197 | log.Printf("concurrent:%d, recv:%d, request:%d room id:%d wait:%d\n", 198 | param.concurrent, param.recv_count, param.count, param.room_id, param.wait) 199 | 200 | seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) 201 | redis_pool = NewRedisPool(redis_address, redis_password, redis_db) 202 | 203 | send_c = make(chan bool, 100) 204 | recv_c = make(chan bool, 10000) 205 | u := int64(1000) 206 | 207 | concurrent := param.concurrent 208 | recv_count := param.recv_count 209 | count := param.count 210 | room_id := int64(param.room_id) 211 | 212 | conns := make([]*net.TCPConn, 0, 1000) 213 | for i := 0; i < recv_count+concurrent; i++ { 214 | conn := ConnectServer(u + int64(i)) 215 | 216 | var room_body Room = Room(room_id) 217 | m := &Message{cmd: MSG_ENTER_ROOM, seq: 2, version: DEFAULT_VERSION, flag: 0, body: &room_body} 218 | SendMessage(conn, m) 219 | 220 | if i < recv_count { 221 | uid := u + int64(i) 222 | go recv(uid, room_id, conn, param.count, param.concurrent) 223 | } 224 | 225 | conns = append(conns, conn) 226 | if i%500 == 0 && i > 0 { 227 | time.Sleep(time.Second * 1) 228 | } 229 | } 230 | 231 | time.Sleep(time.Second * 2) 232 | 233 | fmt.Println("connected") 234 | 235 | begin := time.Now().UnixNano() 236 | for i := 0; i < concurrent; i++ { 237 | uid := u + int64(i+recv_count) 238 | go send(uid, room_id, conns[i+recv_count], param.count, param.concurrent) 239 | } 240 | 241 | for i := 0; i < concurrent; i++ { 242 | <-send_c 243 | } 244 | 245 | end := time.Now().UnixNano() 246 | 247 | var tps int64 = 0 248 | if end-begin > 0 { 249 | tps = int64(1000*1000*1000*concurrent*count) / (end - begin) 250 | } 251 | fmt.Println("tps:", tps) 252 | 253 | fmt.Println("waiting recv completed...") 254 | for i := 0; i < recv_count; i++ { 255 | <-recv_c 256 | } 257 | fmt.Println("recv completed") 258 | } 259 | -------------------------------------------------------------------------------- /benchmark/benchmark_route.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "flag" 8 | "log" 9 | "time" 10 | ) 11 | 12 | var route_addr string = "127.0.0.1:4444" 13 | var appid int64 = 8 14 | 15 | func Dispatch(amsg *RouteMessage) { 16 | log.Printf("amsg appid:%d receiver:%d", amsg.appid, amsg.receiver) 17 | } 18 | func main() { 19 | flag.Parse() 20 | 21 | channel1 := NewChannel(route_addr, Dispatch, nil, nil) 22 | channel1.Start() 23 | 24 | channel1.Subscribe(appid, 1000, true) 25 | 26 | time.Sleep(1 * time.Second) 27 | 28 | channel2 := NewChannel(route_addr, Dispatch, nil, nil) 29 | channel2.Start() 30 | 31 | im := &IMMessage{} 32 | im.sender = 1 33 | im.receiver = 1000 34 | im.content = "test" 35 | msg := &Message{cmd: MSG_IM, body: im} 36 | 37 | mbuffer := new(bytes.Buffer) 38 | WriteMessage(mbuffer, msg) 39 | msg_buf := mbuffer.Bytes() 40 | 41 | amsg := &RouteMessage{} 42 | amsg.appid = appid 43 | amsg.receiver = 1000 44 | amsg.msg = msg_buf 45 | channel2.Publish(amsg) 46 | 47 | time.Sleep(3 * time.Second) 48 | 49 | channel1.Unsubscribe(appid, 1000, true) 50 | 51 | time.Sleep(1 * time.Second) 52 | } 53 | -------------------------------------------------------------------------------- /benchmark/benchmark_sender.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "math/rand" 10 | "net" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/gomodule/redigo/redis" 15 | ) 16 | 17 | var first int64 18 | var last int64 19 | var host string 20 | var port int 21 | var redis_pool *redis.Pool 22 | var seededRand *rand.Rand 23 | 24 | const redis_address = "127.0.0.1:6379" 25 | const redis_password = "" 26 | const redis_db = 0 27 | 28 | func init() { 29 | flag.Int64Var(&first, "first", 0, "first uid") 30 | flag.Int64Var(&last, "last", 0, "last uid") 31 | 32 | flag.StringVar(&host, "host", "127.0.0.1", "host") 33 | flag.IntVar(&port, "port", 23000, "port") 34 | } 35 | 36 | func send(uid int64) { 37 | ip := net.ParseIP(host) 38 | addr := net.TCPAddr{ip, port, ""} 39 | 40 | token, err := login(uid) 41 | if err != nil { 42 | log.Println("login error") 43 | return 44 | } 45 | 46 | conn, err := net.DialTCP("tcp4", nil, &addr) 47 | if err != nil { 48 | log.Println("connect error") 49 | return 50 | } 51 | seq := 1 52 | 53 | auth := &AuthenticationToken{token: token, platform_id: 1, device_id: "00000000"} 54 | SendMessage(conn, &Message{cmd: MSG_AUTH_TOKEN, seq: seq, version: DEFAULT_VERSION, body: auth}) 55 | ReceiveMessage(conn) 56 | 57 | for i := 0; i < 18000; i++ { 58 | r := rand.Int63() 59 | receiver := r%(last-first) + first 60 | log.Println("receiver:", receiver) 61 | content := fmt.Sprintf("test....%d", i) 62 | seq++ 63 | msg := &Message{cmd: MSG_IM, seq: seq, version: DEFAULT_VERSION, flag: 0, body: &IMMessage{uid, receiver, 0, int32(i), content}} 64 | SendMessage(conn, msg) 65 | for { 66 | ack := ReceiveMessage(conn) 67 | if ack.cmd == MSG_ACK { 68 | break 69 | } 70 | } 71 | } 72 | conn.Close() 73 | log.Printf("%d send complete", uid) 74 | } 75 | 76 | func main() { 77 | runtime.GOMAXPROCS(4) 78 | flag.Parse() 79 | fmt.Printf("first:%d, last:%d\n", first, last) 80 | if last <= first { 81 | return 82 | } 83 | log.SetFlags(log.Lshortfile | log.LstdFlags) 84 | 85 | seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) 86 | redis_pool = NewRedisPool(redis_address, redis_password, redis_db) 87 | send(1) 88 | } 89 | -------------------------------------------------------------------------------- /benchmark/benchmark_storage.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "runtime" 10 | "time" 11 | ) 12 | 13 | var storage_address string = "127.0.0.1:13333" 14 | var appid int64 = 8 15 | var device_id int64 = 1 16 | 17 | var concurrent int 18 | var count int 19 | 20 | var c chan bool 21 | 22 | func init() { 23 | flag.IntVar(&concurrent, "c", 10, "concurrent number") 24 | flag.IntVar(&count, "n", 100000, "request number") 25 | } 26 | 27 | func newRPCClient(addr string) *RPCStorage { 28 | rpc_storage := NewRPCStorage([]string{addr}, nil) 29 | return rpc_storage 30 | } 31 | 32 | func Test_SetAndEnqueue() { 33 | dc := newRPCClient(storage_address) 34 | im := &IMMessage{sender: 1, receiver: 1000, content: "1111"} 35 | m := &Message{cmd: MSG_IM, body: im} 36 | msgid, _, err := dc.SaveMessage(appid, 1000, device_id, m) 37 | 38 | if err != nil { 39 | log.Println("save peer message err:", err) 40 | return 41 | } 42 | log.Println("insert msgid:%d", msgid) 43 | } 44 | 45 | func benchmark() { 46 | dc := newRPCClient(storage_address) 47 | 48 | for i := 0; i < count; i++ { 49 | im := &IMMessage{sender: 1, receiver: 1000, content: "1111"} 50 | m := &Message{cmd: MSG_IM, body: im} 51 | 52 | _, _, err := dc.SaveMessage(appid, 1000, device_id, m) 53 | if err != nil { 54 | fmt.Println("save peer message err:", err) 55 | return 56 | } 57 | //msgid := resp.(int64) 58 | //log.Println("msgid:", msgid) 59 | } 60 | 61 | c <- true 62 | 63 | } 64 | 65 | func main() { 66 | runtime.GOMAXPROCS(4) 67 | flag.Parse() 68 | 69 | fmt.Printf("concurrent:%d, request:%d\n", concurrent, count) 70 | log.SetFlags(log.Lshortfile | log.LstdFlags) 71 | 72 | c = make(chan bool, 100) 73 | 74 | begin := time.Now().UnixNano() 75 | 76 | for i := 0; i < concurrent; i++ { 77 | go benchmark() 78 | } 79 | 80 | for i := 0; i < concurrent; i++ { 81 | <-c 82 | } 83 | end := time.Now().UnixNano() 84 | 85 | var tps int64 = 0 86 | if end-begin > 0 { 87 | tps = int64(1000*1000*1000*concurrent*count) / (end - begin) 88 | } 89 | fmt.Println("tps:", tps) 90 | } 91 | -------------------------------------------------------------------------------- /db.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS gobelieve DEFAULT CHARACTER SET utf8; 2 | use gobelieve; 3 | 4 | 5 | CREATE TABLE `group` ( 6 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 7 | `appid` bigint(20) DEFAULT NULL, 8 | `master` bigint(20) DEFAULT NULL, 9 | `super` tinyint(4) NOT NULL DEFAULT '0', 10 | `name` varchar(255) DEFAULT NULL, 11 | `notice` varchar(255) DEFAULT NULL COMMENT '公告', 12 | `deleted` tinyint(1) NOT NULL COMMENT '删除标志', 13 | PRIMARY KEY (`id`) 14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 15 | 16 | CREATE TABLE `group_member` ( 17 | `group_id` bigint(20) NOT NULL DEFAULT '0', 18 | `uid` bigint(20) NOT NULL DEFAULT '0', 19 | `timestamp` int(11) DEFAULT NULL COMMENT '入群时间,单位:秒', 20 | `nickname` varchar(255) DEFAULT NULL COMMENT '群内昵称', 21 | `mute` tinyint(1) DEFAULT '0' COMMENT '群内禁言', 22 | `deleted` tinyint(1) NOT NULL COMMENT '删除标志', 23 | PRIMARY KEY (`group_id`,`uid`), 24 | KEY `idx_group_member_uid` (`uid`) 25 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 26 | 27 | 28 | CREATE TABLE `friend` ( 29 | `appid` bigint(20) NOT NULL, 30 | `uid` bigint(20) NOT NULL, 31 | `friend_uid` bigint(20) NOT NULL, 32 | `timestamp` int(11) NOT NULL COMMENT '创建时间', 33 | PRIMARY KEY (`appid`, `uid`,`friend_uid`) 34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='好友关系'; 35 | 36 | 37 | CREATE TABLE `blacklist` ( 38 | `appid` bigint(20) NOT NULL, 39 | `uid` bigint(20) NOT NULL, 40 | `friend_uid` bigint(20) NOT NULL, 41 | `timestamp` int(11) NOT NULL COMMENT '创建时间', 42 | PRIMARY KEY (`appid`, `uid`,`friend_uid`) 43 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='黑名单'; 44 | 45 | 46 | SHOW TABLES; 47 | 48 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | #token的格式 2 | 3 | 连接im服务器token存储在redis的hash对象中,脱离API服务器测试时,可以手工生成。 4 | $token就是客户端需要获得的, 用来连接im服务器的认证信息。 5 | key:access_token_$token 6 | field: 7 | user_id:用户id 8 | app_id:应用id 9 | 10 | 11 | 12 | #群组通知内容格式: 13 | 14 | 1. 创建群: 15 | 16 | v = { 17 | "group_id":群组id, 18 | "master":群组管理员, 19 | "name":群组名称, 20 | "members":成员列表(long数组), 21 | "timestamp":时间戳(秒) 22 | } 23 | op = {"create":v} 24 | 25 | 26 | 2. 解散群: 27 | 28 | v = { 29 | "group_id":群组id, 30 | "timestamp":时间戳 31 | } 32 | 33 | op = {"disband":v} 34 | 35 | 36 | 3. 更新群组名称: 37 | 38 | v = { 39 | "group_id":群组id, 40 | "timestamp":时间戳, 41 | "name":群组名 42 | } 43 | op = {"update_name":v} 44 | 45 | 4. 添加群组成员: 46 | 47 | 48 | v = { 49 | "group_id":群组id(long类型), 50 | "member_id":新的成员id, 51 | "timestamp":时间戳 52 | } 53 | op = {"add_member":v} 54 | 55 | 56 | 5. 离开群: 57 | 58 | 59 | v = { 60 | "group_id":群组id, 61 | "member_id":离开的成员id, 62 | "timestamp":时间戳 63 | } 64 | op = {"quit_group":v} 65 | 66 | 67 | 6. 普通群升级为超级群: 68 | 69 | 70 | v = { 71 | "group_id":群组id, 72 | "timestamp":时间戳, 73 | "super":1 74 | } 75 | 76 | #群组结构更新广播内容格式: 77 | 78 | 1. 新建群: 79 | 80 | channel名:group_create 81 | 内容格式:"群组id(整型),appid(整型),超级群(1/0)" 82 | 83 | 2. 解散群: 84 | 85 | channel名:group_disband 86 | 内容格式:"群组id" 87 | 88 | 3. 新增成员: 89 | 90 | channel名:group_member_add 91 | 内容格式:"群组id,成员id" 92 | 93 | 4. 删除成员: 94 | 95 | channel名:group_member_remove 96 | 内容格式:"群组id,成员id" 97 | 98 | 5. 普通群升级为超级群: 99 | 100 | channel名:group_upgrade 101 | 内容格式:"群组id(整型),appid(整型),1" 102 | 103 | -------------------------------------------------------------------------------- /doc/design.md: -------------------------------------------------------------------------------- 1 | # 消息存储 2 | 3 | ## 每条消息会存储到每个用户的消息队列,包括发送者和接受者 4 | 所有消息都会保存,每个用户的个人消息(包括他所发送和接受的)只会存储在一台机器上, 5 | 这样能够能够方便的获取到个人的聊天历史记录,同时也导致一条消息会存储两份, 6 | 发送者和接受者各自一份。 7 | 消息存储文件以appendonly的模式打开, 每条新消息会被append到文件尾部。 8 | 9 | 10 | ## 消息接受 11 | 每个用户终端保存上次接受到的消息的synckey, 当有新消息通知或者首次上线时,用synckey去同步服务器的新消息,在收到新消息后, 保存新的synckey, 并继续等待新消息通知。 12 | 以同步的方式获取所有的消息(包括在线,离线),这样能够保证消息的有序和不会丢失,也能够保证多端消息的一致性。 13 | 服务器同时在redis中记录下每个用户最后的synckey, 这样当用户卸载后重新安装app使用服务器保存的synckey来同步新消息。 14 | 15 | 16 | # 服务器扩容 17 | 1. im实例随时可以添加,不需要停机。 18 | 2. imr实例添加的时候,需要改变所有im实例的配置,然后重启所有的im实例。 19 | 3. 添加ims实例: 20 | 一台服务器上可以部署多个ims实例。 21 | ims实例数目只能倍增, 不能单独添加一个实例。 22 | 在添加相同数目的ims实例时,还需要将旧实例的数据按序copy到新实例。 23 | 更改所有im实例的配置,然后重启所有im实例。 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoBelieveIO/im_service 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.4.0 7 | github.com/bitly/go-simplejson v0.5.0 8 | github.com/go-sql-driver/mysql v1.5.0 9 | github.com/golang-jwt/jwt/v4 v4.5.1 10 | github.com/gomodule/redigo v1.8.1 11 | github.com/gorilla/websocket v1.4.2 12 | github.com/importcjj/sensitive v0.0.0-20190611120559-289e87ec4108 13 | github.com/jackc/puddle v1.1.4 14 | github.com/sirupsen/logrus v1.9.3 15 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 16 | ) 17 | 18 | require ( 19 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 20 | github.com/kr/pretty v0.1.0 // indirect 21 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= 4 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= 5 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 6 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 11 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 12 | github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= 13 | github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 14 | github.com/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8= 15 | github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 16 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 17 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 18 | github.com/importcjj/sensitive v0.0.0-20190611120559-289e87ec4108 h1:+8ShsQ1TCRYvZvLlQkekhVDLAHtm/V4nOTZWq1VedbU= 19 | github.com/importcjj/sensitive v0.0.0-20190611120559-289e87ec4108/go.mod h1:zLVdX6Ed2SvCbEamKmve16U0E03UkdJo4ls1TBfmc8Q= 20 | github.com/jackc/puddle v1.1.4 h1:5Ey/o5IfV7dYX6Znivq+N9MdK1S18OJI5OJq6EAAADw= 21 | github.com/jackc/puddle v1.1.4/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 22 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 23 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 24 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 25 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 26 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 30 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 33 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 34 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 35 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 37 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 40 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 41 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 42 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /handler/http_handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package handler 21 | 22 | import "net/http" 23 | 24 | type Handler[T1 any] struct { 25 | handler func(http.ResponseWriter, *http.Request, T1) 26 | arg1 T1 27 | } 28 | 29 | func (handler *Handler[T1]) ServeHTTP(w http.ResponseWriter, r *http.Request) { 30 | handler.handler(w, r, handler.arg1) 31 | } 32 | 33 | func NewHandler[T1 any](handler func(http.ResponseWriter, *http.Request, T1), arg1 T1) *Handler[T1] { 34 | return &Handler[T1]{handler: handler, arg1: arg1} 35 | } 36 | 37 | type Handler2[T1 any, T2 any] struct { 38 | handler func(http.ResponseWriter, *http.Request, T1, T2) 39 | arg1 T1 40 | arg2 T2 41 | } 42 | 43 | func (handler *Handler2[T1, T2]) ServeHTTP(w http.ResponseWriter, r *http.Request) { 44 | handler.handler(w, r, handler.arg1, handler.arg2) 45 | } 46 | 47 | type Handler3[T1 any, T2 any, T3 any] struct { 48 | handler func(http.ResponseWriter, *http.Request, T1, T2, T3) 49 | arg1 T1 50 | arg2 T2 51 | arg3 T3 52 | } 53 | 54 | func (handler *Handler3[T1, T2, T3]) ServeHTTP(w http.ResponseWriter, r *http.Request) { 55 | handler.handler(w, r, handler.arg1, handler.arg2, handler.arg3) 56 | } 57 | 58 | func Handle[T any](pattern string, handler func(http.ResponseWriter, *http.Request, T), arg T) { 59 | http.Handle(pattern, &Handler[T]{handler, arg}) 60 | } 61 | 62 | func Handle2[T1 any, T2 any](pattern string, handler func(http.ResponseWriter, *http.Request, T1, T2), arg1 T1, arg2 T2) { 63 | http.Handle(pattern, &Handler2[T1, T2]{handler, arg1, arg2}) 64 | } 65 | 66 | func Handle3[T1 any, T2 any, T3 any](pattern string, handler func(http.ResponseWriter, *http.Request, T1, T2, T3), arg1 T1, arg2 T2, arg3 T3) { 67 | http.Handle(pattern, &Handler3[T1, T2, T3]{handler, arg1, arg2, arg3}) 68 | } 69 | -------------------------------------------------------------------------------- /handler/logging_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type LoggingHandler struct { 10 | Handler http.Handler 11 | } 12 | 13 | func (h LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 14 | log.Infof("http request:%s %s %s", r.RemoteAddr, r.Method, r.URL) 15 | h.Handler.ServeHTTP(w, r) 16 | } 17 | -------------------------------------------------------------------------------- /im.cfg.sample: -------------------------------------------------------------------------------- 1 | #服务监听端口 2 | port=23000 3 | 4 | #ssl监听端口 可选项 5 | #ssl_port=24430 6 | 7 | #存储服务器地址 "服务器1的ip:port 服务器2的ip:port ..." 多个存储服务器之间用空格隔开,顺序要保证一致 8 | storage_rpc_addrs=["127.0.0.1:13333"] 9 | 10 | #可选项 超级群的存储服务器, 格式和storage_rpc_pool一样 11 | #不配置的情况下,使用storage_rpc_pool 12 | #配置的情况下,不能和storage_rpc_pool有重复 13 | #group_storage_rpc_addrs= 14 | 15 | #路由服务器地址 "服务器1的ip:port 服务器2的ip:port ..." 多个存储服务器之间用空格隔开,顺序要保证一致 16 | route_addrs=["127.0.0.1:4444"] 17 | 18 | #可选项 超级群的路由服务器地址,格式和route_pool一样 19 | #不配置的情况下,使用route_pool 20 | #配置的情况下,一定不能和route_pool有重复 21 | #group_route_addrs= 22 | 23 | 24 | #websocket的监听地址 ip:port 可选项 25 | ws_address=":13891" 26 | #wss监听地址 可选项 27 | #wss_address=":14891" 28 | 29 | #websocket tls域名的证书文件路径(let's encrypt) 可选项 30 | cert_file="fullchain1.pem" 31 | key_file="privkey1.pem" 32 | 33 | 34 | #服务器状态信息和发送群组通知消息的接口监听地址 ip:port ip一般使用内网网卡地址 35 | http_listen_address=":6666" 36 | #grpc监听地址 37 | rpc_listen_address=":6665" 38 | 39 | #mysql的链接地址 用户名:密码@tcp(服务器地址:服务器端口)/数据库名称 40 | mysqldb_datasource="root:123456@tcp(127.0.0.1:3306)/gobelieve" 41 | #redis服务器地址 服务器ip:服务器端口 42 | 43 | #群组消息发送队列的存储路径,必须存在 44 | pending_root="/tmp/pending" 45 | 46 | #客服的appid 可选项 47 | kefu_appid=0 48 | 49 | [redis] 50 | address="127.0.0.1:6379" 51 | password="" 52 | db=0 53 | 54 | #可选日志配置 55 | [log] 56 | filename="im.log" 57 | level="info" 58 | #rotate log保存的文件数 59 | backup=3 60 | #log保存天数 61 | age=30 62 | #是否打印file&line 63 | caller=true 64 | -------------------------------------------------------------------------------- /im/Makefile: -------------------------------------------------------------------------------- 1 | all:im 2 | 3 | im: *.go 4 | go build -ldflags "-X main.VERSION=2.0.0 -X 'main.BUILD_TIME=`date`' -X 'main.GO_VERSION=`go version`' -X 'main.GIT_COMMIT_ID=`git log --pretty=format:"%h" -1`' -X 'main.GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`'" -o ./im 5 | 6 | clean: 7 | rm -f im 8 | -------------------------------------------------------------------------------- /im/auth.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "encoding/json" 24 | "errors" 25 | "fmt" 26 | 27 | "github.com/GoBelieveIO/im_service/server" 28 | "github.com/golang-jwt/jwt/v4" 29 | "github.com/gomodule/redigo/redis" 30 | ) 31 | 32 | type RedisAuth struct { 33 | redis_pool *redis.Pool 34 | } 35 | 36 | func (a *RedisAuth) LoadUserAccessToken(token string) (int64, int64, error) { 37 | conn := a.redis_pool.Get() 38 | defer conn.Close() 39 | 40 | key := fmt.Sprintf("access_token_%s", token) 41 | var uid int64 42 | var appid int64 43 | 44 | err := conn.Send("EXISTS", key) 45 | if err != nil { 46 | return 0, 0, err 47 | } 48 | err = conn.Send("HMGET", key, "user_id", "app_id") 49 | if err != nil { 50 | return 0, 0, err 51 | } 52 | err = conn.Flush() 53 | if err != nil { 54 | return 0, 0, err 55 | } 56 | 57 | exists, err := redis.Bool(conn.Receive()) 58 | if err != nil { 59 | return 0, 0, err 60 | } 61 | reply, err := redis.Values(conn.Receive()) 62 | if err != nil { 63 | return 0, 0, err 64 | } 65 | 66 | if !exists { 67 | return 0, 0, errors.New("token non exists") 68 | } 69 | _, err = redis.Scan(reply, &uid, &appid) 70 | if err != nil { 71 | return 0, 0, err 72 | } 73 | 74 | return appid, uid, nil 75 | } 76 | 77 | type JWTAuth struct { 78 | jwt_signing_key string 79 | } 80 | 81 | func (a *JWTAuth) LoadUserAccessToken(tokenString string) (int64, int64, error) { 82 | var appid, uid int64 83 | p := &jwt.Parser{UseJSONNumber: true} 84 | token, err := p.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 85 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 86 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 87 | } 88 | return a.jwt_signing_key, nil 89 | }) 90 | if err != nil { 91 | return 0, 0, err 92 | } 93 | 94 | if !token.Valid { 95 | return 0, 0, errors.New("invalid token") 96 | } 97 | 98 | if claims, ok := token.Claims.(jwt.MapClaims); ok { 99 | if n, ok := claims["appid"].(json.Number); ok { 100 | appid, err = n.Int64() 101 | if err != nil { 102 | return 0, 0, err 103 | } 104 | } 105 | if n, ok := claims["uid"].(json.Number); ok { 106 | uid, err = n.Int64() 107 | if err != nil { 108 | return 0, 0, err 109 | } 110 | } 111 | return appid, uid, nil 112 | } else { 113 | return 0, 0, errors.New("invalid token") 114 | } 115 | } 116 | 117 | func NewRedisAuth(r *redis.Pool) server.Auth { 118 | return &RedisAuth{redis_pool: r} 119 | } 120 | 121 | func NewJWTAuth() server.Auth { 122 | return &JWTAuth{} 123 | } 124 | 125 | func NewAuth(method string, redis_pool *redis.Pool) server.Auth { 126 | if method == "redis" { 127 | return NewRedisAuth(redis_pool) 128 | } else if method == "jwt" { 129 | return NewJWTAuth() 130 | } else { 131 | return nil 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /im/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "log" 24 | "strconv" 25 | "strings" 26 | 27 | "github.com/BurntSushi/toml" 28 | "github.com/GoBelieveIO/im_service/server" 29 | ) 30 | 31 | const DEFAULT_GROUP_DELIVER_COUNT = 4 32 | 33 | type RedisConfig struct { 34 | Address string `toml:"address"` 35 | Password string `toml:"password"` 36 | Db int `toml:"db"` 37 | } 38 | 39 | type LogConfig struct { 40 | Filename string `toml:"filename"` 41 | Level string `toml:"level"` 42 | Backup int `toml:"backup"` //log files 43 | Age int `toml:"age"` //days 44 | Caller bool `toml:"caller"` 45 | } 46 | 47 | type Config struct { 48 | Port int `toml:"port"` 49 | SslPort int `toml:"ssl_port"` 50 | MySqlDataSource string `toml:"mysqldb_datasource"` 51 | PendingRoot string `toml:"pending_root"` 52 | 53 | KefuAppId int64 `toml:"kefu_appid"` 54 | 55 | Redis RedisConfig `toml:"redis"` 56 | 57 | HttpListenAddress string `toml:"http_listen_address"` 58 | 59 | //websocket listen address 60 | WsAddress string `toml:"ws_address"` 61 | 62 | WssAddress string `toml:"wss_address"` 63 | CertFile string `toml:"cert_file"` 64 | KeyFile string `toml:"key_file"` 65 | 66 | StorageRpcAddrs []string `toml:"storage_rpc_addrs"` 67 | GroupStorageRpcAdrs []string `toml:"group_storage_rpc_addrs"` 68 | RouteAddrs []string `toml:"route_addrs"` 69 | GroupRouteAddrs []string `toml:"group_route_addrs"` //可选配置项, 超群群的route server 70 | 71 | GroupDeliverCount int `toml:"group_deliver_count"` //群组消息投递并发数量,默认4 72 | WordFile string `toml:"word_file"` //关键词字典文件 73 | EnableFriendship bool `toml:"enable_friendship"` //验证好友关系 74 | EnableBlacklist bool `toml:"enable_blacklist"` //验证是否在对方的黑名单中 75 | 76 | MemoryLimit string `toml:"memory_limit"` //rss超过limit,不接受新的链接 77 | 78 | memory_limit int64 79 | 80 | Log LogConfig `toml:"log"` 81 | 82 | AuthMethod string `toml:"auth_method"` //jwt or redis 83 | JwtSigningKey string `toml:"jwt_signing_key"` 84 | 85 | jwt_signing_key []byte 86 | } 87 | 88 | func (config *Config) redis_config() *server.RedisConfig { 89 | return server.NewRedisConfig(config.Redis.Address, config.Redis.Password, config.Redis.Db) 90 | } 91 | 92 | func read_cfg(cfg_path string) *Config { 93 | var conf Config 94 | if _, err := toml.DecodeFile(cfg_path, &conf); err != nil { 95 | // handle error 96 | log.Fatal("Decode cfg file fail:", err) 97 | } 98 | 99 | mem_limit := strings.TrimSpace(conf.MemoryLimit) 100 | if mem_limit != "" { 101 | if strings.HasSuffix(mem_limit, "M") { 102 | mem_limit = mem_limit[0 : len(mem_limit)-1] 103 | n, _ := strconv.ParseInt(mem_limit, 10, 64) 104 | conf.memory_limit = n * 1024 * 1024 105 | } else if strings.HasSuffix(mem_limit, "G") { 106 | mem_limit = mem_limit[0 : len(mem_limit)-1] 107 | n, _ := strconv.ParseInt(mem_limit, 10, 64) 108 | conf.memory_limit = n * 1024 * 1024 * 1024 109 | } 110 | } 111 | 112 | if conf.AuthMethod == "" { 113 | conf.AuthMethod = "redis" 114 | } 115 | 116 | conf.jwt_signing_key = []byte(conf.JwtSigningKey) 117 | return &conf 118 | } 119 | -------------------------------------------------------------------------------- /im/http_server.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "net/http" 24 | 25 | "github.com/gomodule/redigo/redis" 26 | 27 | "github.com/GoBelieveIO/im_service/handler" 28 | "github.com/GoBelieveIO/im_service/server" 29 | log "github.com/sirupsen/logrus" 30 | ) 31 | 32 | func StartHttpServer(addr string, app_route *server.AppRoute, app *server.App, redis_pool *redis.Pool, server_summary *server.ServerSummary, rpc_storage *server.RPCStorage) { 33 | http.HandleFunc("/stack", server.Stack) 34 | handler.Handle2("/summary", server.Summary, app_route, server_summary) 35 | handler.Handle2("/post_group_notification", server.PostGroupNotification, app, rpc_storage) 36 | handler.Handle3("/post_peer_message", server.PostPeerMessage, app, server_summary, rpc_storage) 37 | handler.Handle3("/post_group_message", server.PostGroupMessage, app, server_summary, rpc_storage) 38 | handler.Handle2("/post_system_message", server.SendSystemMessage, app, rpc_storage) 39 | handler.Handle("/post_notification", server.SendNotification, app) 40 | handler.Handle("/post_room_message", server.SendRoomMessage, app) 41 | handler.Handle2("/post_customer_message", server.SendCustomerMessage, app, rpc_storage) 42 | handler.Handle("/post_realtime_message", server.SendRealtimeMessage, app) 43 | handler.Handle2("/get_offline_count", server.GetOfflineCount, redis_pool, rpc_storage) 44 | handler.Handle("/load_latest_message", server.LoadLatestMessage, rpc_storage) 45 | handler.Handle("/load_history_message", server.LoadHistoryMessage, rpc_storage) 46 | 47 | handler := handler.LoggingHandler{Handler: http.DefaultServeMux} 48 | 49 | err := http.ListenAndServe(addr, handler) 50 | if err != nil { 51 | log.Fatal("http server err:", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /im/im_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/GoBelieveIO/im_service/server" 8 | "github.com/gomodule/redigo/redis" 9 | "github.com/importcjj/sensitive" 10 | ) 11 | 12 | func TestFilter(t *testing.T) { 13 | filter := sensitive.New() 14 | 15 | err := filter.LoadWordDict("../bin/dict.txt") 16 | if err != nil { 17 | log.Println("Load word dict err:", err) 18 | } 19 | filter.AddWord("长者") 20 | 21 | msg := &server.IMMessage{} 22 | 23 | //TODO 24 | //msg.content = "{\"text\": \"\\u6211\\u4e3a\\u5171*\\u4ea7\\u515a\\u7eed\\u4e00\\u79d2\"}" 25 | server.FilterDirtyWord(filter, msg) 26 | //log.Println("msg:", string(msg.content)) 27 | 28 | s := "我为共*产党续一秒" 29 | t1 := filter.RemoveNoise(s) 30 | log.Println(filter.Replace(t1, '*')) 31 | e, t2 := filter.FindIn(s) 32 | log.Println(e, t2) 33 | } 34 | 35 | func TestConfig(t *testing.T) { 36 | conf := read_cfg("../bin/im.cfg") 37 | log.Println("config:", conf) 38 | log.Println("redis config:", conf.Redis) 39 | log.Println("log config:", conf.Log) 40 | log.Println("route channels:", conf.RouteAddrs) 41 | } 42 | 43 | func TestRelationship(t *testing.T) { 44 | config := read_cfg("../bin/im.cfg") 45 | redis_pool := NewRedisPool(config.Redis.Address, config.Redis.Password, 46 | config.Redis.Db) 47 | relationship_pool := server.NewRelationshipPool(config.MySqlDataSource, redis_pool) 48 | rs := relationship_pool.GetRelationship(7, 1, 2) 49 | log.Println("rs:", rs, rs.IsMyFriend(), rs.IsYourFriend(), rs.IsInMyBlacklist(), rs.IsInYourBlacklist()) 50 | 51 | relationship_pool.SetMyFriend(7, 1, 2, true) 52 | relationship_pool.SetYourFriend(7, 1, 2, true) 53 | relationship_pool.SetInMyBlacklist(7, 1, 2, true) 54 | relationship_pool.SetInYourBlacklist(7, 1, 2, true) 55 | 56 | rs = relationship_pool.GetRelationship(7, 1, 2) 57 | 58 | log.Println("rs:", rs, rs.IsMyFriend(), rs.IsYourFriend(), rs.IsInMyBlacklist(), rs.IsInYourBlacklist()) 59 | 60 | if !rs.IsMyFriend() || !rs.IsYourFriend() || !rs.IsInMyBlacklist() || !rs.IsInYourBlacklist() { 61 | t.Error("error") 62 | t.FailNow() 63 | } 64 | 65 | log.Println("rs:", rs, rs.IsMyFriend(), rs.IsYourFriend(), rs.IsInMyBlacklist(), rs.IsInYourBlacklist()) 66 | 67 | relationship_pool.SetMyFriend(7, 1, 2, false) 68 | relationship_pool.SetYourFriend(7, 1, 2, false) 69 | relationship_pool.SetInMyBlacklist(7, 1, 2, false) 70 | relationship_pool.SetInYourBlacklist(7, 1, 2, false) 71 | 72 | rs = relationship_pool.GetRelationship(7, 1, 2) 73 | 74 | if rs.IsMyFriend() || rs.IsYourFriend() || rs.IsInMyBlacklist() || rs.IsInYourBlacklist() { 75 | t.Error("error") 76 | t.FailNow() 77 | } 78 | 79 | log.Println("rs:", rs, rs.IsMyFriend(), rs.IsYourFriend(), rs.IsInMyBlacklist(), rs.IsInYourBlacklist()) 80 | 81 | } 82 | 83 | func TestStreamRange(t *testing.T) { 84 | config := read_cfg("../bin/im.cfg") 85 | redis_pool := NewRedisPool(config.Redis.Address, config.Redis.Password, 86 | config.Redis.Db) 87 | conn := redis_pool.Get() 88 | defer conn.Close() 89 | 90 | r, err := redis.Values(conn.Do("XREVRANGE", "test_stream", "+", "-", "COUNT", "1")) 91 | 92 | if err != nil { 93 | log.Println("redis err:", err) 94 | return 95 | } 96 | 97 | for len(r) > 0 { 98 | var entries []interface{} 99 | r, err = redis.Scan(r, &entries) 100 | if err != nil { 101 | t.Error("redis err:", err) 102 | return 103 | } 104 | 105 | var id string 106 | var fields []interface{} 107 | _, err = redis.Scan(entries, &id, &fields) 108 | if err != nil { 109 | t.Error("redis err:", err) 110 | return 111 | } 112 | log.Println("id:", id) 113 | 114 | event := &server.GroupEvent{} 115 | event.Id = id 116 | err = redis.ScanStruct(fields, event) 117 | if err != nil { 118 | log.Println("scan err:", err) 119 | } 120 | log.Println("event:", event.Id, event.Name, event.GroupId, event.MemberId, event.IsSuper) 121 | } 122 | } 123 | 124 | func TestStreamRead(t *testing.T) { 125 | config := read_cfg("../bin/im.cfg") 126 | redis_pool := NewRedisPool(config.Redis.Address, config.Redis.Password, 127 | config.Redis.Db) 128 | 129 | conn := redis_pool.Get() 130 | defer conn.Close() 131 | 132 | reply, err := redis.Values(conn.Do("XREAD", "COUNT", "2", "STREAMS", "test_stream", "0-0")) 133 | 134 | if err != nil { 135 | log.Println("redis err:", err) 136 | return 137 | } 138 | 139 | var stream_res []interface{} 140 | _, err = redis.Scan(reply, &stream_res) 141 | if err != nil { 142 | log.Println("redis scan err:", err) 143 | return 144 | } 145 | 146 | var ss string 147 | var r []interface{} 148 | _, err = redis.Scan(stream_res, &ss, &r) 149 | if err != nil { 150 | log.Println("redis scan err:", err) 151 | return 152 | } 153 | 154 | for len(r) > 0 { 155 | var entries []interface{} 156 | r, err = redis.Scan(r, &entries) 157 | if err != nil { 158 | t.Error("redis err:", err) 159 | return 160 | } 161 | 162 | var id string 163 | var fields []interface{} 164 | _, err = redis.Scan(entries, &id, &fields) 165 | if err != nil { 166 | t.Error("redis err:", err) 167 | return 168 | } 169 | log.Println("id:", id) 170 | 171 | event := &server.GroupEvent{} 172 | event.Id = id 173 | err = redis.ScanStruct(fields, event) 174 | if err != nil { 175 | log.Println("scan err:", err) 176 | } 177 | log.Println("event:", event.Id, event.Name, event.GroupId, event.MemberId, event.IsSuper) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /im/listener.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "crypto/tls" 24 | "fmt" 25 | "net" 26 | "sync/atomic" 27 | 28 | "github.com/gorilla/websocket" 29 | log "github.com/sirupsen/logrus" 30 | 31 | "github.com/GoBelieveIO/im_service/server" 32 | ) 33 | 34 | type Listener struct { 35 | server_summary *server.ServerSummary 36 | low_memory *int32 37 | server *server.Server 38 | } 39 | 40 | func handle_client(conn server.Conn, listener *Listener) { 41 | low := atomic.LoadInt32(listener.low_memory) 42 | if low != 0 { 43 | log.Warning("low memory, drop new connection") 44 | return 45 | } 46 | client := server.NewClient(conn, listener.server_summary, listener.server) 47 | client.Run() 48 | } 49 | 50 | func handle_ws_client(conn *websocket.Conn, listener *Listener) { 51 | handle_client(&server.WSConn{Conn: conn}, listener) 52 | } 53 | 54 | func handle_tcp_client(conn net.Conn, listener *Listener) { 55 | handle_client(&server.NetConn{Conn: conn}, listener) 56 | } 57 | 58 | func ListenClient(port int, listener *Listener) { 59 | listen_addr := fmt.Sprintf("0.0.0.0:%d", port) 60 | listen, err := net.Listen("tcp", listen_addr) 61 | if err != nil { 62 | log.Errorf("listen err:%s", err) 63 | return 64 | } 65 | tcp_listener, ok := listen.(*net.TCPListener) 66 | if !ok { 67 | log.Error("listen err") 68 | return 69 | } 70 | 71 | for { 72 | conn, err := tcp_listener.AcceptTCP() 73 | if err != nil { 74 | log.Errorf("accept err:%s", err) 75 | return 76 | } 77 | log.Infoln("handle new connection, remote address:", conn.RemoteAddr()) 78 | handle_tcp_client(conn, listener) 79 | } 80 | } 81 | 82 | func ListenSSL(port int, cert_file, key_file string, listener *Listener) { 83 | cert, err := tls.LoadX509KeyPair(cert_file, key_file) 84 | if err != nil { 85 | log.Fatal("load cert err:", err) 86 | return 87 | } 88 | config := &tls.Config{Certificates: []tls.Certificate{cert}} 89 | addr := fmt.Sprintf(":%d", port) 90 | listen, err := tls.Listen("tcp", addr, config) 91 | if err != nil { 92 | log.Fatal("ssl listen err:", err) 93 | } 94 | 95 | log.Infof("ssl listen...") 96 | for { 97 | conn, err := listen.Accept() 98 | if err != nil { 99 | log.Fatal("ssl accept err:", err) 100 | } 101 | log.Infoln("handle new ssl connection, remote address:", conn.RemoteAddr()) 102 | handle_tcp_client(conn, listener) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /im/stat_memory.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "os" 24 | "os/exec" 25 | "path" 26 | "runtime" 27 | "strconv" 28 | "strings" 29 | "sync/atomic" 30 | "time" 31 | 32 | log "github.com/sirupsen/logrus" 33 | ) 34 | 35 | func formatStdOut(stdout []byte, userfulIndex int) []string { 36 | eol := "\n" 37 | infoArr := strings.Split(string(stdout), eol)[userfulIndex] 38 | ret := strings.Fields(infoArr) 39 | return ret 40 | } 41 | 42 | func ReadRSSDarwin(pid int) int64 { 43 | args := "-o rss -p" 44 | stdout, _ := exec.Command("ps", args, strconv.Itoa(pid)).Output() 45 | ret := formatStdOut(stdout, 1) 46 | if len(ret) == 0 { 47 | log.Warning("can't find process") 48 | return 0 49 | } 50 | 51 | rss, _ := strconv.ParseInt(ret[0], 10, 64) 52 | return rss * 1024 53 | } 54 | 55 | func ReadRSSLinux(pid int, pagesize int) int64 { 56 | //http://man7.org/linux/man-pages/man5/proc.5.html 57 | procStatFileBytes, err := os.ReadFile(path.Join("/proc", strconv.Itoa(pid), "stat")) 58 | if err != nil { 59 | log.Warning("read file err:", err) 60 | return 0 61 | } 62 | 63 | splitAfter := strings.SplitAfter(string(procStatFileBytes), ")") 64 | 65 | if len(splitAfter) == 0 || len(splitAfter) == 1 { 66 | log.Warning("Can't find process ") 67 | return 0 68 | } 69 | 70 | infos := strings.Split(splitAfter[1], " ") 71 | if len(infos) < 23 { 72 | //impossible 73 | return 0 74 | } 75 | 76 | rss, _ := strconv.ParseInt(infos[22], 10, 64) 77 | return rss * int64(pagesize) 78 | } 79 | 80 | func ReadRSS(platform string, pid int, pagesize int) int64 { 81 | if platform == "linux" { 82 | return ReadRSSLinux(pid, pagesize) 83 | } else if platform == "darwin" { 84 | return ReadRSSDarwin(pid) 85 | } else { 86 | return 0 87 | } 88 | } 89 | 90 | func MemStatService(low_memory *int32, config *Config) { 91 | platform := runtime.GOOS 92 | pagesize := os.Getpagesize() 93 | pid := os.Getpid() 94 | //3 min 95 | ticker := time.NewTicker(time.Second * 60 * 3) 96 | for range ticker.C { 97 | rss := ReadRSS(platform, pid, pagesize) 98 | if rss > config.memory_limit { 99 | atomic.StoreInt32(low_memory, 1) 100 | } else { 101 | atomic.StoreInt32(low_memory, 0) 102 | } 103 | log.Infof("process rss:%dk low memory:%d", rss/1024, *low_memory) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /im/ws.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "net/http" 24 | 25 | "github.com/GoBelieveIO/im_service/handler" 26 | "github.com/gorilla/websocket" 27 | log "github.com/sirupsen/logrus" 28 | ) 29 | 30 | func CheckOrigin(r *http.Request) bool { 31 | // allow all connections by default 32 | return true 33 | } 34 | 35 | var upgrader = websocket.Upgrader{ 36 | ReadBufferSize: 1024, 37 | WriteBufferSize: 1024, 38 | CheckOrigin: CheckOrigin, 39 | } 40 | 41 | func ServeWebsocket(w http.ResponseWriter, r *http.Request, listener *Listener) { 42 | conn, err := upgrader.Upgrade(w, r, nil) 43 | if err != nil { 44 | log.Error("upgrade err:", err) 45 | return 46 | } 47 | conn.SetReadLimit(64 * 1024) 48 | conn.SetPongHandler(func(string) error { 49 | log.Info("brower websocket pong...") 50 | return nil 51 | }) 52 | log.Info("new websocket connection, remote address:", conn.RemoteAddr()) 53 | handle_ws_client(conn, listener) 54 | } 55 | 56 | func StartWSSServer(tls_address string, cert_file string, key_file string, listener *Listener) { 57 | mux := http.NewServeMux() 58 | mux.Handle("/ws", handler.NewHandler[*Listener](ServeWebsocket, listener)) 59 | 60 | if tls_address != "" && cert_file != "" && key_file != "" { 61 | log.Infof("websocket Serving TLS at %s...", tls_address) 62 | err := http.ListenAndServeTLS(tls_address, cert_file, key_file, mux) 63 | if err != nil { 64 | log.Fatalf("listen err:%s", err) 65 | } 66 | } 67 | } 68 | 69 | func StartWSServer(address string, listener *Listener) { 70 | mux := http.NewServeMux() 71 | mux.Handle("/ws", handler.NewHandler[*Listener](ServeWebsocket, listener)) 72 | err := http.ListenAndServe(address, mux) 73 | if err != nil { 74 | log.Fatalf("listen err:%s", err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /im_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import struct 3 | import socket 4 | import threading 5 | import time 6 | import requests 7 | import json 8 | import uuid 9 | import base64 10 | import select 11 | 12 | from im_client import Client 13 | 14 | 15 | #调用app自身的登陆接口获取im服务必须的access token 16 | def login(uid): 17 | #URL = "http://192.168.33.10" 18 | URL = "http://demo.gobelieve.io" 19 | url = URL + "/auth/token" 20 | obj = {"uid":uid, "user_name":str(uid)} 21 | headers = {'Content-Type': 'application/json; charset=UTF-8'} 22 | res = requests.post(url, data=json.dumps(obj), headers=headers) 23 | if res.status_code != 200: 24 | print res.status_code, res.content 25 | return None 26 | obj = json.loads(res.text) 27 | return obj["token"] 28 | 29 | def post_message(token, content): 30 | #URL = "http://127.0.0.1:23002" 31 | URL = "http://api.gobelieve.io" 32 | url = URL + "/messages" 33 | headers = {'Content-Type': 'application/json; charset=UTF-8'} 34 | headers["Authorization"] = "Bearer " + token 35 | obj = {"content":content, "receiver":1, "msgid":0} 36 | res = requests.post(url, data=json.dumps(obj), headers=headers) 37 | if res.status_code != 200: 38 | print "send fail:", res.status_code, res.content 39 | return 40 | print "send success" 41 | 42 | #现在只能获得最近接受到得消息,不能获得发出去得消息 43 | def load_latest_message(token): 44 | #URL = "http://127.0.0.1:23002" 45 | URL = "http://api.gobelieve.io" 46 | url = URL + "/messages?limit=10" 47 | headers = {'Content-Type': 'application/json; charset=UTF-8'} 48 | headers["Authorization"] = "Bearer " + token 49 | 50 | res = requests.get(url, headers=headers) 51 | if res.status_code != 200: 52 | print "send fail:", res.status_code, res.content 53 | return 54 | print res.content 55 | 56 | def send(): 57 | token = login(2) 58 | if not token: 59 | return 60 | obj = {"text":"test"} 61 | post_message(token, json.dumps(obj)) 62 | 63 | def recv(): 64 | token = login(1) 65 | if not token: 66 | return 67 | load_latest_message(token) 68 | while True: 69 | client = Client() 70 | try: 71 | #host可以使用app自己得地址 72 | r = client.connect_server(token, host=None) 73 | if not r: 74 | print "connect fail" 75 | time.sleep(1) 76 | continue 77 | 78 | while True: 79 | print "recv..." 80 | m = client.recv_message() 81 | if not m: 82 | print "connection closed" 83 | break 84 | print "im content:", m.content 85 | except Exception as e: 86 | print "exception:", e 87 | 88 | def main(): 89 | send() 90 | recv() 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /imr.cfg.sample: -------------------------------------------------------------------------------- 1 | #监听地址 ip:port ip一般使用内网网卡地址 2 | listen=":4444" 3 | 4 | #redis服务器地址 服务器地址:服务器端口 5 | [redis] 6 | address="127.0.0.1:6379" 7 | password="" 8 | db=0 9 | 10 | 11 | -------------------------------------------------------------------------------- /imr/Makefile: -------------------------------------------------------------------------------- 1 | all:imr 2 | 3 | imr:*.go 4 | go build -ldflags "-X main.VERSION=2.0.0 -X 'main.BUILD_TIME=`date`' -X 'main.GO_VERSION=`go version`' -X 'main.GIT_COMMIT_ID=`git log --pretty=format:"%h" -1`' -X 'main.GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`'" -o imr 5 | 6 | 7 | clean: 8 | rm -f imr 9 | -------------------------------------------------------------------------------- /imr/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "log" 24 | 25 | "github.com/BurntSushi/toml" 26 | ) 27 | 28 | type RedisConfig struct { 29 | Address string `toml:"address"` 30 | Password string `toml:"password"` 31 | Db int `toml:"db"` 32 | } 33 | 34 | type LogConfig struct { 35 | Filename string `toml:"filename"` 36 | Level string `toml:"level"` 37 | Backup int `toml:"backup"` //log files 38 | Age int `toml:"age"` //days 39 | Caller bool `toml:"caller"` 40 | } 41 | 42 | type Config struct { 43 | Listen string `toml:"listen"` 44 | 45 | PushDisabled bool `toml:"push_disabled"` 46 | HttpListenAddress string `toml:"http_listen_address"` 47 | 48 | Redis RedisConfig `toml:"redis"` 49 | Log LogConfig `toml:"log"` 50 | } 51 | 52 | func read_route_cfg(cfg_path string) *Config { 53 | var conf Config 54 | if _, err := toml.DecodeFile(cfg_path, &conf); err != nil { 55 | // handle error 56 | log.Fatal("Decode cfg file fail:", err) 57 | } 58 | return &conf 59 | } 60 | -------------------------------------------------------------------------------- /imr/main.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "flag" 24 | "fmt" 25 | "net" 26 | "net/http" 27 | "runtime" 28 | "time" 29 | 30 | "gopkg.in/natefinch/lumberjack.v2" 31 | 32 | "github.com/GoBelieveIO/im_service/handler" 33 | "github.com/GoBelieveIO/im_service/router" 34 | "github.com/gomodule/redigo/redis" 35 | log "github.com/sirupsen/logrus" 36 | ) 37 | 38 | var ( 39 | VERSION string 40 | BUILD_TIME string 41 | GO_VERSION string 42 | GIT_COMMIT_ID string 43 | GIT_BRANCH string 44 | ) 45 | 46 | func handle_client(conn *net.TCPConn, server *router.Server) { 47 | conn.SetKeepAlive(true) 48 | conn.SetKeepAlivePeriod(time.Duration(10 * 60 * time.Second)) 49 | client := router.NewClient(conn, server) 50 | log.Info("new client:", conn.RemoteAddr()) 51 | server.AddClient(client) 52 | client.Run() 53 | } 54 | 55 | func Listen(f func(*net.TCPConn, *router.Server), listen_addr string, server *router.Server) { 56 | listen, err := net.Listen("tcp", listen_addr) 57 | if err != nil { 58 | fmt.Println("初始化失败", err.Error()) 59 | return 60 | } 61 | tcp_listener, ok := listen.(*net.TCPListener) 62 | if !ok { 63 | fmt.Println("listen error") 64 | return 65 | } 66 | 67 | for { 68 | client, err := tcp_listener.AcceptTCP() 69 | if err != nil { 70 | return 71 | } 72 | f(client, server) 73 | } 74 | } 75 | 76 | func ListenClient(server *router.Server, config *Config) { 77 | Listen(handle_client, config.Listen, server) 78 | } 79 | 80 | func NewRedisPool(server, password string, db int) *redis.Pool { 81 | return &redis.Pool{ 82 | MaxIdle: 100, 83 | MaxActive: 500, 84 | IdleTimeout: 480 * time.Second, 85 | Dial: func() (redis.Conn, error) { 86 | timeout := time.Duration(2) * time.Second 87 | c, err := redis.Dial("tcp", server, redis.DialConnectTimeout(timeout)) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if len(password) > 0 { 92 | if _, err := c.Do("AUTH", password); err != nil { 93 | c.Close() 94 | return nil, err 95 | } 96 | } 97 | if db > 0 && db < 16 { 98 | if _, err := c.Do("SELECT", db); err != nil { 99 | c.Close() 100 | return nil, err 101 | } 102 | } 103 | return c, err 104 | }, 105 | } 106 | } 107 | 108 | func StartHttpServer(addr string, server *router.Server) { 109 | handler.Handle("/online", router.GetOnlineStatus, server) 110 | handler.Handle("/all_online", router.GetOnlineClients, server) 111 | 112 | handler := handler.LoggingHandler{Handler: http.DefaultServeMux} 113 | 114 | err := http.ListenAndServe(addr, handler) 115 | if err != nil { 116 | log.Fatal("http server err:", err) 117 | } 118 | } 119 | 120 | func initLog(config *Config) { 121 | if config.Log.Filename != "" { 122 | writer := &lumberjack.Logger{ 123 | Filename: config.Log.Filename, 124 | MaxSize: 1024, // megabytes 125 | MaxBackups: config.Log.Backup, 126 | MaxAge: config.Log.Age, //days 127 | Compress: false, 128 | } 129 | log.SetOutput(writer) 130 | log.StandardLogger().SetNoLock() 131 | } 132 | 133 | log.SetReportCaller(config.Log.Caller) 134 | 135 | level := config.Log.Level 136 | if level == "debug" { 137 | log.SetLevel(log.DebugLevel) 138 | } else if level == "info" { 139 | log.SetLevel(log.InfoLevel) 140 | } else if level == "warn" { 141 | log.SetLevel(log.WarnLevel) 142 | } else if level == "fatal" { 143 | log.SetLevel(log.FatalLevel) 144 | } 145 | } 146 | 147 | func main() { 148 | fmt.Printf("Version: %s\nBuilt: %s\nGo version: %s\nGit branch: %s\nGit commit: %s\n", VERSION, BUILD_TIME, GO_VERSION, GIT_BRANCH, GIT_COMMIT_ID) 149 | 150 | runtime.GOMAXPROCS(runtime.NumCPU()) 151 | flag.Parse() 152 | if len(flag.Args()) == 0 { 153 | fmt.Println("usage: im config") 154 | return 155 | } 156 | 157 | config := read_route_cfg(flag.Args()[0]) 158 | 159 | initLog(config) 160 | 161 | log.Info("startup...") 162 | 163 | log.Infof("listen:%s\n", config.Listen) 164 | 165 | log.Infof("redis address:%s password:%s db:%d\n", 166 | config.Redis.Address, config.Redis.Password, config.Redis.Db) 167 | 168 | log.Infof("push disabled:%t", config.PushDisabled) 169 | 170 | log.Infof("log filename:%s level:%s backup:%d age:%d caller:%t", 171 | config.Log.Filename, config.Log.Level, config.Log.Backup, config.Log.Age, config.Log.Caller) 172 | 173 | redis_pool := NewRedisPool(config.Redis.Address, config.Redis.Password, 174 | config.Redis.Db) 175 | 176 | server := router.NewServer(redis_pool, config.PushDisabled) 177 | server.RunPushService() 178 | if config.HttpListenAddress != "" { 179 | go StartHttpServer(config.HttpListenAddress, server) 180 | } 181 | 182 | ListenClient(server, config) 183 | } 184 | -------------------------------------------------------------------------------- /ims.cfg.sample: -------------------------------------------------------------------------------- 1 | #监听地址 ip:port ip一般选择内网网卡的地址 2 | rpc_listen=":13333" 3 | #消息存储路径,必须存在 4 | storage_root="/tmp/im" 5 | #备份监听地址 ip:port, ip一般选择内网网卡的地址 6 | sync_listen=":3334" 7 | 8 | #主机监听地址,备机需要此配置 9 | #master_address=":3334" 10 | 11 | #客服的appid 12 | kefu_appid=0 13 | -------------------------------------------------------------------------------- /ims/Makefile: -------------------------------------------------------------------------------- 1 | all:ims 2 | 3 | ims: *.go 4 | go build -ldflags "-X main.VERSION=2.0.0 -X 'main.BUILD_TIME=`date`' -X 'main.GO_VERSION=`go version`' -X 'main.GIT_COMMIT_ID=`git log --pretty=format:"%h" -1`' -X 'main.GIT_BRANCH=`git rev-parse --abbrev-ref HEAD`'" -o ./ims 5 | 6 | clean: 7 | rm -f ims main.test 8 | -------------------------------------------------------------------------------- /ims/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "log" 24 | 25 | "github.com/BurntSushi/toml" 26 | ) 27 | 28 | // 超级群离线消息数量限制,超过的部分会被丢弃 29 | const GROUP_OFFLINE_LIMIT = 100 30 | 31 | // 离线消息返回的数量限制 32 | const OFFLINE_DEFAULT_LIMIT = 3000 33 | 34 | const GROUP_OFFLINE_DEFAULT_LIMIT = 0 35 | 36 | // unlimit 37 | const OFFLINE_DEFAULT_HARD_LIMIT = 0 38 | 39 | type LogConfig struct { 40 | Filename string `toml:"filename"` 41 | Level string `toml:"level"` 42 | Backup int `toml:"backup"` //log files 43 | Age int `toml:"age"` //days 44 | Caller bool `toml:"caller"` 45 | } 46 | 47 | type Config struct { 48 | RpcListen string `toml:"rpc_listen"` 49 | StorageRoot string `toml:"storage_root"` 50 | HttpListenAddress string `toml:"http_listen_address"` 51 | 52 | SyncListen string `toml:"sync_listen"` 53 | MasterAddress string `toml:"master_address"` 54 | GroupLimit int `toml:"group_limit"` //普通群离线消息的数量限制 55 | Limit int `toml:"limit"` //单次离线消息的数量限制 56 | HardLimit int `toml:"hard_limit"` //离线消息总的数量限制 57 | 58 | Log LogConfig `toml:"log"` 59 | } 60 | 61 | func read_storage_cfg(cfg_path string) *Config { 62 | var conf Config 63 | if _, err := toml.DecodeFile(cfg_path, &conf); err != nil { 64 | // handle error 65 | log.Fatal("Decode cfg file fail:", err) 66 | } 67 | if conf.Limit == 0 { 68 | conf.Limit = OFFLINE_DEFAULT_LIMIT 69 | } 70 | if conf.GroupLimit == 0 { 71 | conf.GroupLimit = GROUP_OFFLINE_DEFAULT_LIMIT 72 | } 73 | if conf.HardLimit == 0 { 74 | conf.HardLimit = OFFLINE_DEFAULT_HARD_LIMIT 75 | } 76 | return &conf 77 | } 78 | -------------------------------------------------------------------------------- /ims/monitoring.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "runtime" 8 | "runtime/pprof" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type ServerSummary struct { 14 | nrequests int64 15 | peer_message_count int64 16 | group_message_count int64 17 | } 18 | 19 | func NewServerSummary() *ServerSummary { 20 | s := new(ServerSummary) 21 | return s 22 | } 23 | 24 | func Summary(rw http.ResponseWriter, req *http.Request, server_summary *ServerSummary) { 25 | obj := make(map[string]interface{}) 26 | obj["goroutine_count"] = runtime.NumGoroutine() 27 | obj["request_count"] = server_summary.nrequests 28 | obj["peer_message_count"] = server_summary.peer_message_count 29 | obj["group_message_count"] = server_summary.group_message_count 30 | 31 | res, err := json.Marshal(obj) 32 | if err != nil { 33 | log.Info("json marshal:", err) 34 | return 35 | } 36 | 37 | rw.Header().Add("Content-Type", "application/json") 38 | _, err = rw.Write(res) 39 | if err != nil { 40 | log.Info("write err:", err) 41 | } 42 | } 43 | 44 | func Stack(rw http.ResponseWriter, req *http.Request) { 45 | pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 46 | rw.WriteHeader(200) 47 | } 48 | 49 | func WriteHttpError(status int, err string, w http.ResponseWriter) { 50 | w.Header().Set("Content-Type", "application/json") 51 | obj := make(map[string]interface{}) 52 | meta := make(map[string]interface{}) 53 | meta["code"] = status 54 | meta["message"] = err 55 | obj["meta"] = meta 56 | b, _ := json.Marshal(obj) 57 | w.WriteHeader(status) 58 | w.Write(b) 59 | } 60 | 61 | func WriteHttpObj(data map[string]interface{}, w http.ResponseWriter) { 62 | w.Header().Set("Content-Type", "application/json") 63 | obj := make(map[string]interface{}) 64 | obj["data"] = data 65 | b, _ := json.Marshal(obj) 66 | w.Write(b) 67 | } 68 | -------------------------------------------------------------------------------- /ims/rpc.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "sync/atomic" 24 | 25 | . "github.com/GoBelieveIO/im_service/protocol" 26 | rpc_storage "github.com/GoBelieveIO/im_service/storage" 27 | ) 28 | 29 | type RPCStorage struct { 30 | server_summary *ServerSummary 31 | storage *rpc_storage.Storage 32 | limit int 33 | hard_limit int 34 | } 35 | 36 | func (rpc *RPCStorage) SyncMessage(sync_key *rpc_storage.SyncHistory, result *rpc_storage.PeerHistoryMessage) error { 37 | atomic.AddInt64(&rpc.server_summary.nrequests, 1) 38 | messages, last_msgid, hasMore := rpc.storage.LoadHistoryMessagesV3(sync_key.AppID, sync_key.Uid, sync_key.LastMsgID, rpc.limit, rpc.hard_limit) 39 | 40 | historyMessages := make([]*rpc_storage.HistoryMessage, 0, 10) 41 | for _, emsg := range messages { 42 | hm := &rpc_storage.HistoryMessage{} 43 | hm.MsgID = emsg.MsgId 44 | hm.DeviceID = emsg.DeviceId 45 | hm.Cmd = int32(emsg.Msg.Cmd) 46 | 47 | emsg.Msg.Version = DEFAULT_VERSION 48 | hm.Raw = emsg.Msg.ToData() 49 | historyMessages = append(historyMessages, hm) 50 | } 51 | 52 | result.Messages = historyMessages 53 | result.LastMsgID = last_msgid 54 | result.HasMore = hasMore 55 | return nil 56 | } 57 | 58 | func (rpc *RPCStorage) SyncGroupMessage(sync_key *rpc_storage.SyncGroupHistory, result *rpc_storage.GroupHistoryMessage) error { 59 | atomic.AddInt64(&rpc.server_summary.nrequests, 1) 60 | messages, last_msgid := rpc.storage.LoadGroupHistoryMessages(sync_key.AppID, sync_key.Uid, sync_key.GroupID, sync_key.LastMsgID, sync_key.Timestamp, GROUP_OFFLINE_LIMIT) 61 | 62 | historyMessages := make([]*rpc_storage.HistoryMessage, 0, 10) 63 | for _, emsg := range messages { 64 | hm := &rpc_storage.HistoryMessage{} 65 | hm.MsgID = emsg.MsgId 66 | hm.DeviceID = emsg.DeviceId 67 | hm.Cmd = int32(emsg.Msg.Cmd) 68 | 69 | emsg.Msg.Version = DEFAULT_VERSION 70 | hm.Raw = emsg.Msg.ToData() 71 | historyMessages = append(historyMessages, hm) 72 | } 73 | 74 | result.Messages = historyMessages 75 | result.LastMsgID = last_msgid 76 | result.HasMore = false 77 | return nil 78 | } 79 | 80 | func (rpc *RPCStorage) SavePeerMessage(m *rpc_storage.PeerMessage, result *rpc_storage.HistoryMessageID) error { 81 | atomic.AddInt64(&rpc.server_summary.nrequests, 1) 82 | atomic.AddInt64(&rpc.server_summary.peer_message_count, 1) 83 | msg := &Message{Cmd: int(m.Cmd), Version: DEFAULT_VERSION} 84 | msg.FromData(m.Raw) 85 | msgid, prev_msgid := rpc.storage.SavePeerMessage(m.AppID, m.Uid, m.DeviceID, msg) 86 | result.MsgID = msgid 87 | result.PrevMsgID = prev_msgid 88 | return nil 89 | } 90 | 91 | func (rpc *RPCStorage) SavePeerGroupMessage(m *rpc_storage.PeerGroupMessage, result *rpc_storage.GroupHistoryMessageID) error { 92 | atomic.AddInt64(&rpc.server_summary.nrequests, 1) 93 | atomic.AddInt64(&rpc.server_summary.peer_message_count, 1) 94 | msg := &Message{Cmd: int(m.Cmd), Version: DEFAULT_VERSION} 95 | msg.FromData(m.Raw) 96 | r := rpc.storage.SavePeerGroupMessage(m.AppID, m.Members, m.DeviceID, msg) 97 | 98 | result.MessageIDs = make([]*rpc_storage.HistoryMessageID, 0, len(r)/2) 99 | for i := 0; i < len(r); i += 2 { 100 | msgid, prev_msgid := r[i], r[i+1] 101 | result.MessageIDs = append(result.MessageIDs, &rpc_storage.HistoryMessageID{MsgID: msgid, PrevMsgID: prev_msgid}) 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (rpc *RPCStorage) SaveGroupMessage(m *rpc_storage.GroupMessage, result *rpc_storage.HistoryMessageID) error { 108 | atomic.AddInt64(&rpc.server_summary.nrequests, 1) 109 | atomic.AddInt64(&rpc.server_summary.group_message_count, 1) 110 | msg := &Message{Cmd: int(m.Cmd), Version: DEFAULT_VERSION} 111 | msg.FromData(m.Raw) 112 | msgid, prev_msgid := rpc.storage.SaveGroupMessage(m.AppID, m.GroupID, m.DeviceID, msg) 113 | result.MsgID = msgid 114 | result.PrevMsgID = prev_msgid 115 | return nil 116 | } 117 | 118 | func (rpc *RPCStorage) GetNewCount(sync_key *rpc_storage.SyncHistory, new_count *int64) error { 119 | atomic.AddInt64(&rpc.server_summary.nrequests, 1) 120 | count := rpc.storage.GetNewCount(sync_key.AppID, sync_key.Uid, sync_key.LastMsgID) 121 | *new_count = int64(count) 122 | return nil 123 | } 124 | 125 | func (rpc *RPCStorage) GetLatestMessage(r *rpc_storage.HistoryRequest, l *rpc_storage.LatestMessage) error { 126 | atomic.AddInt64(&rpc.server_summary.nrequests, 1) 127 | messages := rpc.storage.LoadLatestMessages(r.AppID, r.Uid, int(r.Limit)) 128 | 129 | historyMessages := make([]*rpc_storage.HistoryMessage, 0, 10) 130 | for _, emsg := range messages { 131 | hm := &rpc_storage.HistoryMessage{} 132 | hm.MsgID = emsg.MsgId 133 | hm.DeviceID = emsg.DeviceId 134 | hm.Cmd = int32(emsg.Msg.Cmd) 135 | 136 | emsg.Msg.Version = DEFAULT_VERSION 137 | hm.Raw = emsg.Msg.ToData() 138 | historyMessages = append(historyMessages, hm) 139 | } 140 | l.Messages = historyMessages 141 | 142 | return nil 143 | } 144 | 145 | func (rpc *RPCStorage) Ping(int, *int) error { 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /ims/storage_test.go: -------------------------------------------------------------------------------- 1 | //go:build exclude 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | var storage *Storage 14 | var appid int64 = 0 15 | var device_id int64 = 0 16 | var master *Master 17 | var config *StorageConfig 18 | 19 | func init() { 20 | master = NewMaster() 21 | master.Start() 22 | 23 | config = &StorageConfig{} 24 | } 25 | 26 | func TestMain(m *testing.M) { 27 | flag.Parse() 28 | storage = NewStorage("/tmp") 29 | os.Exit(m.Run()) 30 | } 31 | 32 | func Test_Storage(t *testing.T) { 33 | im := &IMMessage{sender: 1, receiver: 2, content: "test"} 34 | msg := &Message{cmd: MSG_IM, body: im} 35 | msgid := storage.SaveMessage(msg) 36 | msg2 := storage.LoadMessage(msgid) 37 | if msg2 != nil { 38 | log.Println("msg2 cmd:", Command(msg2.cmd)) 39 | } else { 40 | log.Println("can't load msg:", msgid) 41 | } 42 | } 43 | 44 | func Test_LoadLatest(t *testing.T) { 45 | im := &IMMessage{sender: 1, receiver: 2, content: "test"} 46 | msg := &Message{cmd: MSG_IM, body: im} 47 | storage.SavePeerMessage(appid, im.receiver, device_id, msg) 48 | 49 | im = &IMMessage{sender: 1, receiver: 2, content: "test2"} 50 | msg = &Message{cmd: MSG_IM, body: im} 51 | storage.SavePeerMessage(appid, im.receiver, device_id, msg) 52 | 53 | messages := storage.LoadLatestMessages(appid, im.receiver, 2) 54 | latest := messages[0] 55 | im2 := latest.msg.body.(*IMMessage) 56 | log.Println("sender:", im2.sender, " receiver:", im2.receiver, " content:", string(im2.content)) 57 | 58 | latest = messages[1] 59 | im2 = latest.msg.body.(*IMMessage) 60 | log.Println("sender:", im2.sender, " receiver:", im2.receiver, " content:", string(im2.content)) 61 | 62 | } 63 | 64 | func Test_Sync(t *testing.T) { 65 | last_id, _ := storage.GetLastMessageID(appid, 2) 66 | 67 | im := &IMMessage{sender: 1, receiver: 2, content: "test"} 68 | msg := &Message{cmd: MSG_IM, body: im} 69 | storage.SavePeerMessage(appid, im.receiver, device_id, msg) 70 | 71 | im = &IMMessage{sender: 1, receiver: 2, content: "test2"} 72 | msg = &Message{cmd: MSG_IM, body: im} 73 | storage.SavePeerMessage(appid, im.receiver, device_id, msg) 74 | 75 | messages, _, _ := storage.LoadHistoryMessagesV3(appid, im.receiver, last_id, 0, 0) 76 | latest := messages[0] 77 | im2 := latest.msg.body.(*IMMessage) 78 | log.Println("sender:", im2.sender, " receiver:", im2.receiver, " content:", string(im2.content)) 79 | 80 | latest = messages[1] 81 | im2 = latest.msg.body.(*IMMessage) 82 | log.Println("sender:", im2.sender, " receiver:", im2.receiver, " content:", string(im2.content)) 83 | } 84 | 85 | func Test_SyncBatch(t *testing.T) { 86 | receiver := int64(2) 87 | last_id, _ := storage.GetLastMessageID(appid, receiver) 88 | 89 | for i := 0; i < 5000; i++ { 90 | content := fmt.Sprintf("test:%d", i) 91 | im := &IMMessage{sender: 1, receiver: receiver, content: content} 92 | msg := &Message{cmd: MSG_IM, body: im} 93 | storage.SavePeerMessage(appid, im.receiver, device_id, msg) 94 | } 95 | 96 | hasMore := true 97 | loop := 0 98 | for hasMore { 99 | messages, last_msgid, m := storage.LoadHistoryMessagesV3(appid, receiver, last_id, 1000, 4000) 100 | latest := messages[0] 101 | im2 := latest.msg.body.(*IMMessage) 102 | log.Println("loop:", loop, "sender:", im2.sender, " receiver:", im2.receiver, " content:", string(im2.content)) 103 | 104 | loop++ 105 | last_id = last_msgid 106 | hasMore = m 107 | } 108 | } 109 | 110 | func Test_PeerIndex(t *testing.T) { 111 | storage.flushIndex() 112 | } 113 | 114 | func Test_NewCount(t *testing.T) { 115 | receiver := int64(2) 116 | 117 | content := "test" 118 | im := &IMMessage{sender: 1, receiver: receiver, content: content} 119 | msg := &Message{cmd: MSG_IM, body: im} 120 | last_id, _ := storage.SavePeerMessage(appid, im.receiver, device_id, msg) 121 | 122 | for i := 0; i < 5000; i++ { 123 | content = fmt.Sprintf("test:%d", i) 124 | im = &IMMessage{sender: 1, receiver: receiver, content: content} 125 | msg = &Message{cmd: MSG_IM, body: im} 126 | storage.SavePeerMessage(appid, im.receiver, device_id, msg) 127 | } 128 | 129 | count := storage.GetNewCount(appid, receiver, last_id) 130 | if count != 5000 { 131 | t.Errorf("new count = %d; expected %d", count, 5000) 132 | } else { 133 | log.Println("last id:", last_id, " new count:", count) 134 | } 135 | 136 | count = storage.GetNewCount(appid, receiver, 0) 137 | log.Println("last id:", 0, " new count:", count) 138 | } 139 | -------------------------------------------------------------------------------- /lru/lru.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package lru implements an LRU cache. 18 | package lru 19 | 20 | import "container/list" 21 | 22 | // Cache is an LRU cache. It is not safe for concurrent access. 23 | type Cache struct { 24 | // MaxEntries is the maximum number of cache entries before 25 | // an item is evicted. Zero means no limit. 26 | MaxEntries int 27 | 28 | // OnEvicted optionally specificies a callback function to be 29 | // executed when an entry is purged from the cache. 30 | OnEvicted func(key Key, value interface{}) 31 | 32 | ll *list.List 33 | cache map[interface{}]*list.Element 34 | } 35 | 36 | // A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators 37 | type Key interface{} 38 | 39 | type entry struct { 40 | key Key 41 | value interface{} 42 | } 43 | 44 | // New creates a new Cache. 45 | // If maxEntries is zero, the cache has no limit and it's assumed 46 | // that eviction is done by the caller. 47 | func New(maxEntries int) *Cache { 48 | return &Cache{ 49 | MaxEntries: maxEntries, 50 | ll: list.New(), 51 | cache: make(map[interface{}]*list.Element), 52 | } 53 | } 54 | 55 | // Add adds a value to the cache. 56 | func (c *Cache) Add(key Key, value interface{}) { 57 | if c.cache == nil { 58 | c.cache = make(map[interface{}]*list.Element) 59 | c.ll = list.New() 60 | } 61 | if ee, ok := c.cache[key]; ok { 62 | c.ll.MoveToFront(ee) 63 | ee.Value.(*entry).value = value 64 | return 65 | } 66 | ele := c.ll.PushFront(&entry{key, value}) 67 | c.cache[key] = ele 68 | if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { 69 | c.RemoveOldest() 70 | } 71 | } 72 | 73 | // Get looks up a key's value from the cache. 74 | func (c *Cache) Get(key Key) (value interface{}, ok bool) { 75 | if c.cache == nil { 76 | return 77 | } 78 | if ele, hit := c.cache[key]; hit { 79 | c.ll.MoveToFront(ele) 80 | return ele.Value.(*entry).value, true 81 | } 82 | return 83 | } 84 | 85 | // Remove removes the provided key from the cache. 86 | func (c *Cache) Remove(key Key) { 87 | if c.cache == nil { 88 | return 89 | } 90 | if ele, hit := c.cache[key]; hit { 91 | c.removeElement(ele) 92 | } 93 | } 94 | 95 | // RemoveOldest removes the oldest item from the cache. 96 | func (c *Cache) RemoveOldest() { 97 | if c.cache == nil { 98 | return 99 | } 100 | ele := c.ll.Back() 101 | if ele != nil { 102 | c.removeElement(ele) 103 | } 104 | } 105 | 106 | func (c *Cache) removeElement(e *list.Element) { 107 | c.ll.Remove(e) 108 | kv := e.Value.(*entry) 109 | delete(c.cache, kv.key) 110 | if c.OnEvicted != nil { 111 | c.OnEvicted(kv.key, kv.value) 112 | } 113 | } 114 | 115 | // Len returns the number of items in the cache. 116 | func (c *Cache) Len() int { 117 | if c.cache == nil { 118 | return 0 119 | } 120 | return c.ll.Len() 121 | } 122 | -------------------------------------------------------------------------------- /protocol/command.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package protocol 21 | 22 | import "fmt" 23 | 24 | // 接入服务器消息 25 | const MSG_AUTH_STATUS = 3 26 | const MSG_ACK = 5 27 | const MSG_PING = 13 28 | const MSG_PONG = 14 29 | const MSG_AUTH_TOKEN = 15 30 | const MSG_RT = 17 31 | const MSG_ENTER_ROOM = 18 32 | const MSG_LEAVE_ROOM = 19 33 | const MSG_ROOM_IM = 20 34 | const MSG_UNREAD_COUNT = 22 35 | 36 | // persistent, deprecated 37 | const MSG_CUSTOMER_SERVICE_ = 23 38 | 39 | // 客户端->服务端 40 | const MSG_SYNC = 26 //同步消息 41 | // 服务端->客服端 42 | const MSG_SYNC_BEGIN = 27 43 | const MSG_SYNC_END = 28 44 | 45 | // 通知客户端有新消息 46 | const MSG_SYNC_NOTIFY = 29 47 | 48 | // 客户端->服务端 49 | const MSG_SYNC_GROUP = 30 //同步超级群消息 50 | // 服务端->客服端 51 | const MSG_SYNC_GROUP_BEGIN = 31 52 | const MSG_SYNC_GROUP_END = 32 53 | 54 | // 通知客户端有新消息 55 | const MSG_SYNC_GROUP_NOTIFY = 33 56 | 57 | // 客服端->服务端,更新服务器的synckey 58 | const MSG_SYNC_KEY = 34 59 | const MSG_GROUP_SYNC_KEY = 35 60 | 61 | // 系统通知消息, unpersistent 62 | const MSG_NOTIFICATION = 36 63 | 64 | // 消息的meta信息 65 | const MSG_METADATA = 37 66 | 67 | // im实例使用 68 | const MSG_PENDING_GROUP_MESSAGE = 251 69 | 70 | // 服务器消息, 被所有服务器使用 71 | // persistent 点对点消息 72 | const MSG_IM = 4 73 | 74 | // persistent 75 | const MSG_GROUP_NOTIFICATION = 7 76 | const MSG_GROUP_IM = 8 77 | 78 | // persistent 79 | const MSG_SYSTEM = 21 80 | 81 | // persistent, deprecated 82 | const MSG_CUSTOMER_ = 24 //顾客->客服 83 | const MSG_CUSTOMER_SUPPORT_ = 25 //客服->顾客 84 | 85 | // persistent 不同app间的点对点消息 86 | const MSG_CUSTOMER_V2 = 64 87 | 88 | // 路由服务器消息 89 | const MSG_SUBSCRIBE = 130 90 | const MSG_UNSUBSCRIBE = 131 91 | const MSG_PUBLISH = 132 92 | 93 | const MSG_PUSH = 134 94 | const MSG_PUBLISH_GROUP = 135 95 | 96 | const MSG_SUBSCRIBE_ROOM = 136 97 | const MSG_UNSUBSCRIBE_ROOM = 137 98 | const MSG_PUBLISH_ROOM = 138 99 | 100 | // 主从同步消息 101 | const MSG_STORAGE_SYNC_BEGIN = 220 102 | const MSG_STORAGE_SYNC_MESSAGE = 221 103 | const MSG_STORAGE_SYNC_MESSAGE_BATCH = 222 104 | 105 | // 内部文件存储使用 106 | // 超级群消息队列 代替MSG_GROUP_IM_LIST 107 | const MSG_GROUP_OFFLINE = 247 108 | 109 | // 个人消息队列 代替MSG_OFFLINE_V3 110 | const MSG_OFFLINE_V4 = 248 111 | 112 | // 个人消息队列 代替MSG_OFFLINE_V2 113 | const MSG_OFFLINE_V3_ = 249 114 | 115 | // 个人消息队列 代替MSG_OFFLINE 116 | // deprecated 兼容性 117 | const MSG_OFFLINE_V2_ = 250 118 | 119 | // im实例使用 120 | const ___MSG_PENDING_GROUP_MESSAGE___ = 251 121 | 122 | // 超级群消息队列 123 | // deprecated 兼容性 124 | const MSG_GROUP_IM_LIST_ = 252 125 | 126 | // deprecated 127 | const MSG_GROUP_ACK_IN_ = 253 128 | 129 | // deprecated 兼容性 130 | const MSG_OFFLINE_ = 254 131 | 132 | // deprecated 133 | const MSG_ACK_IN_ = 255 134 | 135 | type Command int 136 | 137 | func (cmd Command) String() string { 138 | c := int(cmd) 139 | if desc, ok := message_descriptions[c]; ok { 140 | return desc 141 | } else { 142 | return fmt.Sprintf("%d", c) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /protocol/protocol.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package protocol 21 | 22 | import ( 23 | "bytes" 24 | "encoding/binary" 25 | "io" 26 | 27 | "encoding/hex" 28 | "errors" 29 | 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | func WriteHeader(len int32, seq int32, cmd byte, version byte, flag byte, buffer io.Writer) { 34 | binary.Write(buffer, binary.BigEndian, len) 35 | binary.Write(buffer, binary.BigEndian, seq) 36 | t := []byte{cmd, byte(version), flag, 0} 37 | buffer.Write(t) 38 | } 39 | 40 | func ReadHeader(buff []byte) (int, int, int, int, int) { 41 | var length int32 42 | var seq int32 43 | buffer := bytes.NewBuffer(buff) 44 | binary.Read(buffer, binary.BigEndian, &length) 45 | binary.Read(buffer, binary.BigEndian, &seq) 46 | cmd, _ := buffer.ReadByte() 47 | version, _ := buffer.ReadByte() 48 | flag, _ := buffer.ReadByte() 49 | return int(length), int(seq), int(cmd), int(version), int(flag) 50 | } 51 | 52 | func WriteMessage(w *bytes.Buffer, msg *Message) { 53 | body := msg.ToData() 54 | WriteHeader(int32(len(body)), int32(msg.Seq), byte(msg.Cmd), byte(msg.Version), byte(msg.Flag), w) 55 | w.Write(body) 56 | } 57 | 58 | func SendMessage(conn io.Writer, msg *Message) error { 59 | buffer := new(bytes.Buffer) 60 | WriteMessage(buffer, msg) 61 | buf := buffer.Bytes() 62 | n, err := conn.Write(buf) 63 | if err != nil { 64 | log.Info("sock write error:", err) 65 | return err 66 | } 67 | if n != len(buf) { 68 | log.Infof("write less:%d %d", n, len(buf)) 69 | return errors.New("write less") 70 | } 71 | return nil 72 | } 73 | 74 | func ReceiveLimitMessage(conn io.Reader, limit_size int, external bool) (*Message, error) { 75 | buff := make([]byte, 12) 76 | _, err := io.ReadFull(conn, buff) 77 | if err != nil { 78 | log.Info("sock read error:", err) 79 | return nil, err 80 | } 81 | 82 | length, seq, cmd, version, flag := ReadHeader(buff) 83 | if length < 0 || length >= limit_size { 84 | log.Info("invalid len:", length) 85 | return nil, errors.New("invalid length") 86 | } 87 | 88 | //0 <= cmd <= 255 89 | //收到客户端非法消息,断开链接 90 | if external && !external_messages[cmd] { 91 | log.Warning("invalid external message cmd:", Command(cmd)) 92 | return nil, errors.New("invalid cmd") 93 | } 94 | 95 | buff = make([]byte, length) 96 | _, err = io.ReadFull(conn, buff) 97 | if err != nil { 98 | log.Info("sock read error:", err) 99 | return nil, err 100 | } 101 | 102 | message := new(Message) 103 | message.Cmd = cmd 104 | message.Seq = seq 105 | message.Version = version 106 | message.Flag = flag 107 | if !message.FromData(buff) { 108 | log.Warningf("parse error:%d, %d %d %d %s", cmd, seq, version, 109 | flag, hex.EncodeToString(buff)) 110 | return nil, errors.New("parse error") 111 | } 112 | return message, nil 113 | } 114 | 115 | func ReceiveMessage(conn io.Reader) *Message { 116 | m, _ := ReceiveLimitMessage(conn, 32*1024, false) 117 | return m 118 | } 119 | 120 | // used by benchmark 121 | func ReceiveServerMessage(conn io.Reader) (*Message, error) { 122 | return ReceiveLimitMessage(conn, 32*1024, false) 123 | } 124 | 125 | // 接受客户端消息(external messages) 126 | func ReceiveClientMessage(conn io.Reader) (*Message, error) { 127 | return ReceiveLimitMessage(conn, 32*1024, true) 128 | } 129 | 130 | // 消息大小限制在32M 131 | func ReceiveStorageSyncMessage(conn io.Reader) *Message { 132 | m, _ := ReceiveLimitMessage(conn, 32*1024*1024, false) 133 | return m 134 | } 135 | 136 | // 消息大小限制在1M 137 | func ReceiveStorageMessage(conn io.Reader) *Message { 138 | m, _ := ReceiveLimitMessage(conn, 1024*1024, false) 139 | return m 140 | } 141 | -------------------------------------------------------------------------------- /router/app_route.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package router 21 | 22 | import ( 23 | "sync" 24 | 25 | "github.com/GoBelieveIO/im_service/set" 26 | ) 27 | 28 | type AppRoute struct { 29 | mutex sync.Mutex 30 | apps map[int64]*Route 31 | } 32 | 33 | func NewAppRoute() *AppRoute { 34 | app_route := new(AppRoute) 35 | app_route.apps = make(map[int64]*Route) 36 | return app_route 37 | } 38 | 39 | func (app_route *AppRoute) FindOrAddRoute(appid int64) *Route { 40 | app_route.mutex.Lock() 41 | defer app_route.mutex.Unlock() 42 | if route, ok := app_route.apps[appid]; ok { 43 | return route 44 | } 45 | route := NewRoute(appid) 46 | app_route.apps[appid] = route 47 | return route 48 | } 49 | 50 | func (app_route *AppRoute) FindRoute(appid int64) *Route { 51 | app_route.mutex.Lock() 52 | defer app_route.mutex.Unlock() 53 | return app_route.apps[appid] 54 | } 55 | 56 | func (app_route *AppRoute) AddRoute(route *Route) { 57 | app_route.mutex.Lock() 58 | defer app_route.mutex.Unlock() 59 | app_route.apps[route.appid] = route 60 | } 61 | 62 | func (app_route *AppRoute) GetUsers() map[int64]set.IntSet { 63 | app_route.mutex.Lock() 64 | defer app_route.mutex.Unlock() 65 | 66 | r := make(map[int64]set.IntSet) 67 | for appid, route := range app_route.apps { 68 | uids := route.GetUserIDs() 69 | r[appid] = uids 70 | } 71 | return r 72 | } 73 | 74 | type ClientSet = set.Set[*Client] 75 | 76 | func NewClientSet() ClientSet { 77 | return set.NewSet[*Client]() 78 | } 79 | -------------------------------------------------------------------------------- /router/client.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package router 21 | 22 | import ( 23 | "net" 24 | 25 | log "github.com/sirupsen/logrus" 26 | 27 | "github.com/GoBelieveIO/im_service/protocol" 28 | ) 29 | 30 | type ClientObserver interface { 31 | onClientMessage(client *Client, msg *protocol.Message) 32 | onClientClose(client *Client) 33 | } 34 | 35 | type Client struct { 36 | wt chan *protocol.Message 37 | 38 | conn *net.TCPConn 39 | app_route *AppRoute 40 | 41 | observer ClientObserver 42 | } 43 | 44 | func NewClient(conn *net.TCPConn, observer ClientObserver) *Client { 45 | client := new(Client) 46 | client.conn = conn 47 | client.wt = make(chan *protocol.Message, 10) 48 | client.app_route = NewAppRoute() 49 | client.observer = observer 50 | return client 51 | } 52 | 53 | func (client *Client) ContainAppUserID(id *RouteUserID) bool { 54 | route := client.app_route.FindRoute(id.appid) 55 | if route == nil { 56 | return false 57 | } 58 | 59 | return route.ContainUserID(id.uid) 60 | } 61 | 62 | func (client *Client) IsAppUserOnline(id *RouteUserID) bool { 63 | route := client.app_route.FindRoute(id.appid) 64 | if route == nil { 65 | return false 66 | } 67 | 68 | return route.IsUserOnline(id.uid) 69 | } 70 | 71 | func (client *Client) ContainAppRoomID(id *RouteRoomID) bool { 72 | route := client.app_route.FindRoute(id.appid) 73 | if route == nil { 74 | return false 75 | } 76 | 77 | return route.ContainRoomID(id.room_id) 78 | } 79 | 80 | func (client *Client) Read() { 81 | for { 82 | msg := client.read() 83 | if msg == nil { 84 | client.observer.onClientClose(client) 85 | client.wt <- nil 86 | break 87 | } 88 | client.observer.onClientMessage(client, msg) 89 | } 90 | } 91 | 92 | func (client *Client) Write() { 93 | seq := 0 94 | for { 95 | msg := <-client.wt 96 | if msg == nil { 97 | client.close() 98 | log.Infof("client socket closed") 99 | break 100 | } 101 | seq++ 102 | msg.Seq = seq 103 | client.send(msg) 104 | } 105 | } 106 | 107 | func (client *Client) Run() { 108 | go client.Write() 109 | go client.Read() 110 | } 111 | 112 | func (client *Client) read() *protocol.Message { 113 | return protocol.ReceiveMessage(client.conn) 114 | } 115 | 116 | func (client *Client) send(msg *protocol.Message) { 117 | protocol.SendMessage(client.conn, msg) 118 | } 119 | 120 | func (client *Client) close() { 121 | client.conn.Close() 122 | } 123 | -------------------------------------------------------------------------------- /router/route.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package router 21 | 22 | import ( 23 | "sync" 24 | 25 | "github.com/GoBelieveIO/im_service/set" 26 | ) 27 | 28 | type Route struct { 29 | appid int64 30 | mutex sync.Mutex 31 | uids map[int64]bool 32 | room_ids set.IntSet 33 | } 34 | 35 | func NewRoute(appid int64) *Route { 36 | r := new(Route) 37 | r.appid = appid 38 | r.uids = make(map[int64]bool) 39 | r.room_ids = set.NewIntSet() 40 | return r 41 | } 42 | 43 | func (route *Route) ContainUserID(uid int64) bool { 44 | route.mutex.Lock() 45 | defer route.mutex.Unlock() 46 | 47 | _, ok := route.uids[uid] 48 | return ok 49 | } 50 | 51 | func (route *Route) IsUserOnline(uid int64) bool { 52 | route.mutex.Lock() 53 | defer route.mutex.Unlock() 54 | 55 | return route.uids[uid] 56 | } 57 | 58 | func (route *Route) AddUserID(uid int64, online bool) { 59 | route.mutex.Lock() 60 | defer route.mutex.Unlock() 61 | 62 | route.uids[uid] = online 63 | } 64 | 65 | func (route *Route) RemoveUserID(uid int64) { 66 | route.mutex.Lock() 67 | defer route.mutex.Unlock() 68 | 69 | delete(route.uids, uid) 70 | } 71 | 72 | func (route *Route) GetUserIDs() set.IntSet { 73 | route.mutex.Lock() 74 | defer route.mutex.Unlock() 75 | 76 | uids := set.NewIntSet() 77 | for uid := range route.uids { 78 | uids.Add(uid) 79 | } 80 | return uids 81 | } 82 | 83 | func (route *Route) ContainRoomID(room_id int64) bool { 84 | route.mutex.Lock() 85 | defer route.mutex.Unlock() 86 | 87 | return route.room_ids.IsMember(room_id) 88 | } 89 | 90 | func (route *Route) AddRoomID(room_id int64) { 91 | route.mutex.Lock() 92 | defer route.mutex.Unlock() 93 | 94 | route.room_ids.Add(room_id) 95 | } 96 | 97 | func (route *Route) RemoveRoomID(room_id int64) { 98 | route.mutex.Lock() 99 | defer route.mutex.Unlock() 100 | 101 | route.room_ids.Remove(room_id) 102 | } 103 | -------------------------------------------------------------------------------- /router/rpc.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package router 21 | 22 | import ( 23 | "encoding/json" 24 | "net/http" 25 | "net/url" 26 | "strconv" 27 | 28 | "github.com/GoBelieveIO/im_service/set" 29 | log "github.com/sirupsen/logrus" 30 | ) 31 | 32 | func WriteHttpObj(data map[string]interface{}, w http.ResponseWriter) { 33 | w.Header().Set("Content-Type", "application/json") 34 | obj := make(map[string]interface{}) 35 | obj["data"] = data 36 | b, _ := json.Marshal(obj) 37 | w.Write(b) 38 | } 39 | 40 | func WriteHttpError(status int, err string, w http.ResponseWriter) { 41 | w.Header().Set("Content-Type", "application/json") 42 | obj := make(map[string]interface{}) 43 | meta := make(map[string]interface{}) 44 | meta["code"] = status 45 | meta["message"] = err 46 | obj["meta"] = meta 47 | b, _ := json.Marshal(obj) 48 | w.WriteHeader(status) 49 | w.Write(b) 50 | } 51 | 52 | // 获取当前所有在线的用户 53 | func GetOnlineClients(w http.ResponseWriter, req *http.Request, server *Server) { 54 | clients := server.GetClientSet() 55 | 56 | type App struct { 57 | AppId int64 `json:"appid"` 58 | Users []int64 `json:"users"` 59 | } 60 | 61 | r := make(map[int64]set.IntSet) 62 | for c := range clients { 63 | app_users := c.app_route.GetUsers() 64 | for appid, users := range app_users { 65 | if _, ok := r[appid]; !ok { 66 | r[appid] = set.NewIntSet() 67 | } 68 | uids := r[appid] 69 | for uid := range users { 70 | uids.Add(uid) 71 | } 72 | } 73 | } 74 | 75 | apps := make([]*App, 0, len(r)) 76 | for appid, users := range r { 77 | app := &App{} 78 | app.AppId = appid 79 | app.Users = make([]int64, 0, len(users)) 80 | for uid := range users { 81 | app.Users = append(app.Users, uid) 82 | } 83 | apps = append(apps, app) 84 | } 85 | 86 | res, err := json.Marshal(apps) 87 | if err != nil { 88 | log.Info("json marshal:", err) 89 | WriteHttpError(400, "json marshal err", w) 90 | return 91 | } 92 | w.Header().Add("Content-Type", "application/json") 93 | _, err = w.Write(res) 94 | if err != nil { 95 | log.Info("write err:", err) 96 | } 97 | } 98 | 99 | // 获取单个用户在线状态 100 | func GetOnlineStatus(w http.ResponseWriter, req *http.Request, server *Server) { 101 | log.Info("get user online status") 102 | m, _ := url.ParseQuery(req.URL.RawQuery) 103 | 104 | appid, err := strconv.ParseInt(m.Get("appid"), 10, 64) 105 | if err != nil { 106 | log.Info("error:", err) 107 | WriteHttpError(400, "invalid query param", w) 108 | return 109 | } 110 | 111 | uid, err := strconv.ParseInt(m.Get("uid"), 10, 64) 112 | if err != nil { 113 | log.Info("error:", err) 114 | WriteHttpError(400, "invalid query param", w) 115 | return 116 | } 117 | 118 | online := server.IsUserOnline(appid, uid) 119 | resp := make(map[string]interface{}) 120 | resp["online"] = online 121 | 122 | w.Header().Set("Content-Type", "application/json") 123 | b, _ := json.Marshal(resp) 124 | w.Write(b) 125 | } 126 | -------------------------------------------------------------------------------- /server/app.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "sync/atomic" 24 | 25 | . "github.com/GoBelieveIO/im_service/protocol" 26 | ) 27 | 28 | type RouteChannel interface { 29 | Address() string 30 | Subscribe(appid int64, uid int64, online bool) 31 | Unsubscribe(appid int64, uid int64, online bool) 32 | PublishMessage(appid int64, uid int64, msg *Message) 33 | PublishGroupMessage(appid int64, group_id int64, msg *Message) 34 | PublishRoomMessage(appid int64, room_id int64, m *Message) 35 | Push(appid int64, receivers []int64, msg *Message) 36 | SubscribeRoom(appid int64, room_id int64) 37 | UnsubscribeRoom(appid int64, room_id int64) 38 | } 39 | 40 | type App struct { 41 | app_route *AppRoute 42 | 43 | // route server 44 | route_channels []RouteChannel 45 | // super group route server 46 | group_route_channels []RouteChannel 47 | 48 | current_deliver_index uint64 49 | group_message_delivers []*GroupMessageDeliver 50 | 51 | group_loaders []*GroupLoader 52 | } 53 | 54 | func (app *App) Init( 55 | app_route *AppRoute, 56 | route_channels []RouteChannel, 57 | group_route_channels []RouteChannel, 58 | group_message_delivers []*GroupMessageDeliver, 59 | group_loaders []*GroupLoader) { 60 | app.app_route = app_route 61 | app.route_channels = route_channels 62 | app.group_route_channels = group_route_channels 63 | app.group_message_delivers = group_message_delivers 64 | app.group_loaders = group_loaders 65 | } 66 | 67 | func (app *App) GetChannel(uid int64) RouteChannel { 68 | if uid < 0 { 69 | uid = -uid 70 | } 71 | index := uid % int64(len(app.route_channels)) 72 | return app.route_channels[index] 73 | } 74 | 75 | func (app *App) GetGroupChannel(group_id int64) RouteChannel { 76 | if group_id < 0 { 77 | group_id = -group_id 78 | } 79 | index := group_id % int64(len(app.group_route_channels)) 80 | return app.group_route_channels[index] 81 | } 82 | 83 | func (app *App) GetRoomChannel(room_id int64) RouteChannel { 84 | if room_id < 0 { 85 | room_id = -room_id 86 | } 87 | index := room_id % int64(len(app.route_channels)) 88 | return app.route_channels[index] 89 | } 90 | 91 | func (app *App) GetGroupLoader(group_id int64) *GroupLoader { 92 | if group_id < 0 { 93 | group_id = -group_id 94 | } 95 | 96 | index := uint64(group_id) % uint64(len(app.group_message_delivers)) 97 | return app.group_loaders[index] 98 | } 99 | 100 | func (app *App) GetGroupMessageDeliver(group_id int64) *GroupMessageDeliver { 101 | deliver_index := atomic.AddUint64(&app.current_deliver_index, 1) 102 | index := deliver_index % uint64(len(app.group_message_delivers)) 103 | return app.group_message_delivers[index] 104 | } 105 | 106 | // 群消息通知(apns, gcm...) 107 | func (app *App) PushGroupMessage(appid int64, group *Group, m *Message) { 108 | channels := make(map[RouteChannel][]int64) 109 | members := group.Members() 110 | for member := range members { 111 | //不对自身推送 112 | if im, ok := m.Body.(*IMMessage); ok { 113 | if im.sender == member { 114 | continue 115 | } 116 | } 117 | channel := app.GetChannel(member) 118 | if _, ok := channels[channel]; !ok { 119 | channels[channel] = []int64{member} 120 | } else { 121 | receivers := channels[channel] 122 | receivers = append(receivers, member) 123 | channels[channel] = receivers 124 | } 125 | } 126 | 127 | for channel, receivers := range channels { 128 | channel.Push(appid, receivers, m) 129 | } 130 | } 131 | 132 | // 离线消息推送 133 | func (app *App) PushMessage(appid int64, uid int64, m *Message) { 134 | channel := app.GetChannel(uid) 135 | channel.Push(appid, []int64{uid}, m) 136 | } 137 | 138 | func (app *App) SendAnonymousGroupMessage(appid int64, group *Group, msg *Message) { 139 | app.SendGroupMessage(appid, group, msg, nil) 140 | } 141 | 142 | func (app *App) SendAnonymousMessage(appid int64, uid int64, msg *Message) { 143 | app.SendMessage(appid, uid, msg, nil) 144 | } 145 | 146 | func (app *App) SendAnonymousRoomMessage(appid int64, room_id int64, msg *Message) { 147 | app.SendRoomMessage(appid, room_id, msg, nil) 148 | } 149 | 150 | func (app *App) SendGroupMessage(appid int64, group *Group, msg *Message, sender *Sender) { 151 | app.PublishGroupMessage(appid, group.gid, msg) 152 | var sender_id int64 153 | var device_ID int64 = INVALID_DEVICE_ID 154 | if sender != nil { 155 | sender_id = sender.uid 156 | device_ID = sender.deviceID 157 | } 158 | 159 | app.app_route.sendGroupMessage(appid, sender_id, device_ID, group, msg) 160 | } 161 | 162 | func (app *App) SendMessage(appid int64, uid int64, msg *Message, sender *Sender) { 163 | app.PublishMessage(appid, uid, msg) 164 | var sender_appid int64 165 | var sender_id int64 166 | var device_ID int64 = INVALID_DEVICE_ID 167 | if sender != nil { 168 | sender_appid = sender.appid 169 | sender_id = sender.uid 170 | device_ID = sender.deviceID 171 | } 172 | 173 | app.app_route.sendPeerMessage(sender_appid, sender_id, device_ID, appid, uid, msg) 174 | } 175 | 176 | func (app *App) SendRoomMessage(appid int64, room_id int64, msg *Message, sender *Sender) { 177 | app.PublishRoomMessage(appid, room_id, msg) 178 | var sender_id int64 179 | var device_ID int64 = INVALID_DEVICE_ID 180 | if sender != nil { 181 | sender_id = sender.uid 182 | device_ID = sender.deviceID 183 | } 184 | app.app_route.sendRoomMessage(appid, sender_id, device_ID, room_id, msg) 185 | } 186 | 187 | func (app *App) PublishMessage(appid int64, uid int64, msg *Message) { 188 | channel := app.GetChannel(uid) 189 | channel.PublishMessage(appid, uid, msg) 190 | } 191 | 192 | func (app *App) PublishGroupMessage(appid int64, group_id int64, msg *Message) { 193 | channel := app.GetGroupChannel(group_id) 194 | channel.PublishGroupMessage(appid, group_id, msg) 195 | } 196 | 197 | func (app *App) PublishRoomMessage(appid int64, room_id int64, m *Message) { 198 | channel := app.GetRoomChannel(room_id) 199 | channel.PublishRoomMessage(appid, room_id, m) 200 | } 201 | -------------------------------------------------------------------------------- /server/app_route.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "sync" 24 | 25 | "github.com/GoBelieveIO/im_service/set" 26 | 27 | log "github.com/sirupsen/logrus" 28 | 29 | "github.com/GoBelieveIO/im_service/protocol" 30 | ) 31 | 32 | const INVALID_DEVICE_ID = -1 33 | 34 | type Sender struct { 35 | appid int64 36 | uid int64 37 | deviceID int64 38 | } 39 | type AppRoute struct { 40 | mutex sync.Mutex 41 | apps map[int64]*Route 42 | } 43 | 44 | func NewAppRoute() *AppRoute { 45 | app_route := new(AppRoute) 46 | app_route.apps = make(map[int64]*Route) 47 | return app_route 48 | } 49 | 50 | func (app_route *AppRoute) FindOrAddRoute(appid int64) *Route { 51 | app_route.mutex.Lock() 52 | defer app_route.mutex.Unlock() 53 | if route, ok := app_route.apps[appid]; ok { 54 | return route 55 | } 56 | route := NewRoute(appid) 57 | app_route.apps[appid] = route 58 | return route 59 | } 60 | 61 | func (app_route *AppRoute) FindRoute(appid int64) *Route { 62 | app_route.mutex.Lock() 63 | defer app_route.mutex.Unlock() 64 | return app_route.apps[appid] 65 | } 66 | 67 | func (app_route *AppRoute) AddRoute(route *Route) { 68 | app_route.mutex.Lock() 69 | defer app_route.mutex.Unlock() 70 | app_route.apps[route.appid] = route 71 | } 72 | 73 | func (app_route *AppRoute) GetUsers() map[int64]set.IntSet { 74 | app_route.mutex.Lock() 75 | defer app_route.mutex.Unlock() 76 | 77 | r := make(map[int64]set.IntSet) 78 | for appid, route := range app_route.apps { 79 | uids := route.GetUserIDs() 80 | r[appid] = uids 81 | } 82 | return r 83 | } 84 | 85 | func (app_route *AppRoute) SendGroupMessage(appid int64, group *Group, msg *protocol.Message) bool { 86 | return app_route.sendGroupMessage(appid, 0, INVALID_DEVICE_ID, group, msg) 87 | } 88 | 89 | func (app_route *AppRoute) sendGroupMessage(appid int64, sender int64, device_ID int64, group *Group, msg *protocol.Message) bool { 90 | if group == nil { 91 | return false 92 | } 93 | 94 | route := app_route.FindRoute(appid) 95 | if route == nil { 96 | log.Warningf("can't dispatch app message, appid:%d uid:%d cmd:%s", appid, group.gid, protocol.Command(msg.Cmd)) 97 | return false 98 | } 99 | 100 | members := group.Members() 101 | for member := range members { 102 | clients := route.FindClientSet(member) 103 | if len(clients) == 0 { 104 | continue 105 | } 106 | 107 | for c := range clients { 108 | //不再发送给自己 109 | if c.device_ID == device_ID && sender == c.uid { 110 | continue 111 | } 112 | c.EnqueueNonBlockMessage(msg) 113 | } 114 | } 115 | 116 | return true 117 | } 118 | 119 | func (app_route *AppRoute) sendPeerMessage(sender_appid int64, sender int64, device_ID int64, appid int64, uid int64, msg *protocol.Message) bool { 120 | route := app_route.FindRoute(appid) 121 | if route == nil { 122 | log.Warningf("can't dispatch app message, appid:%d uid:%d cmd:%s", appid, uid, protocol.Command(msg.Cmd)) 123 | return false 124 | } 125 | clients := route.FindClientSet(uid) 126 | if len(clients) == 0 { 127 | return false 128 | } 129 | 130 | for c := range clients { 131 | //不再发送给自己 132 | if c.device_ID == device_ID && sender == c.uid && sender_appid == c.appid { 133 | continue 134 | } 135 | c.EnqueueNonBlockMessage(msg) 136 | } 137 | return true 138 | } 139 | 140 | func (app_route *AppRoute) SendPeerMessage(appid int64, uid int64, msg *protocol.Message) bool { 141 | return app_route.sendPeerMessage(appid, 0, INVALID_DEVICE_ID, appid, uid, msg) 142 | } 143 | 144 | func (app_route *AppRoute) sendRoomMessage(appid int64, sender int64, device_ID int64, room_id int64, msg *protocol.Message) bool { 145 | route := app_route.FindOrAddRoute(appid) 146 | clients := route.FindRoomClientSet(room_id) 147 | 148 | if len(clients) == 0 { 149 | return false 150 | } 151 | for c := range clients { 152 | //不再发送给自己 153 | if c.device_ID == device_ID && sender == c.uid { 154 | continue 155 | } 156 | c.EnqueueNonBlockMessage(msg) 157 | } 158 | return true 159 | } 160 | 161 | func (app_route *AppRoute) SendRoomMessage(appid int64, room_id int64, msg *protocol.Message) bool { 162 | return app_route.sendRoomMessage(appid, 0, INVALID_DEVICE_ID, room_id, msg) 163 | } 164 | -------------------------------------------------------------------------------- /server/client.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "sync/atomic" 24 | "time" 25 | 26 | "container/list" 27 | 28 | log "github.com/sirupsen/logrus" 29 | 30 | . "github.com/GoBelieveIO/im_service/protocol" 31 | ) 32 | 33 | type ClientObserver interface { 34 | onClientMessage(*Client, *Message) 35 | onClientClose(*Client) 36 | } 37 | 38 | type Client struct { 39 | Connection //必须放在结构体首部 40 | observer ClientObserver 41 | } 42 | 43 | func NewClient(conn Conn, server_summary *ServerSummary, observer ClientObserver) *Client { 44 | client := new(Client) 45 | 46 | //初始化Connection 47 | client.conn = conn // conn is net.Conn or engineio.Conn 48 | client.wt = make(chan *Message, 300) 49 | //'10'对于用户拥有非常多的超级群,读线程还是有可能会阻塞 50 | client.pwt = make(chan []*Message, 10) 51 | 52 | client.lwt = make(chan int, 1) //only need 1 53 | client.messages = list.New() 54 | client.server_summary = server_summary 55 | client.observer = observer 56 | 57 | return client 58 | } 59 | 60 | func (client *Client) onclose() { 61 | atomic.AddInt64(&client.server_summary.nconnections, -1) 62 | if client.uid > 0 { 63 | atomic.AddInt64(&client.server_summary.nclients, -1) 64 | } 65 | atomic.StoreInt32(&client.closed, 1) 66 | 67 | //write goroutine will quit when it receives nil 68 | client.wt <- nil 69 | client.observer.onClientClose(client) 70 | } 71 | 72 | func (client *Client) Read() { 73 | atomic.AddInt64(&client.server_summary.nconnections, 1) 74 | for { 75 | tc := atomic.LoadInt32(&client.tc) 76 | if tc > 0 { 77 | log.Infof("quit read goroutine, client:%d write goroutine blocked", client.uid) 78 | client.onclose() 79 | break 80 | } 81 | 82 | t1 := time.Now().Unix() 83 | msg := client.read() 84 | t2 := time.Now().Unix() 85 | if t2-t1 > 6*60 { 86 | log.Infof("client:%d socket read timeout:%d %d", client.uid, t1, t2) 87 | } 88 | if msg == nil { 89 | client.onclose() 90 | break 91 | } 92 | 93 | client.observer.onClientMessage(client, msg) 94 | t3 := time.Now().Unix() 95 | if t3-t2 > 2 { 96 | log.Infof("client:%d handle message is too slow:%d %d", client.uid, t2, t3) 97 | } 98 | } 99 | 100 | } 101 | 102 | // 发送等待队列中的消息 103 | func (client *Client) SendMessages() { 104 | var messages *list.List 105 | client.mutex.Lock() 106 | if client.messages.Len() == 0 { 107 | client.mutex.Unlock() 108 | return 109 | } 110 | messages = client.messages 111 | client.messages = list.New() 112 | client.mutex.Unlock() 113 | 114 | e := messages.Front() 115 | for e != nil { 116 | msg := e.Value.(*Message) 117 | if msg.Cmd == MSG_RT || msg.Cmd == MSG_IM || 118 | msg.Cmd == MSG_GROUP_IM || msg.Cmd == MSG_ROOM_IM { 119 | atomic.AddInt64(&client.server_summary.out_message_count, 1) 120 | } 121 | 122 | if msg.Meta != nil { 123 | meta_msg := &Message{Cmd: MSG_METADATA, Version: client.version, Body: msg.Meta} 124 | client.send(meta_msg) 125 | } 126 | client.send(msg) 127 | e = e.Next() 128 | } 129 | } 130 | 131 | func (client *Client) Write() { 132 | running := true 133 | 134 | //发送在线消息 135 | for running { 136 | select { 137 | case msg := <-client.wt: 138 | if msg == nil { 139 | client.close() 140 | running = false 141 | log.Infof("client:%d socket closed", client.uid) 142 | break 143 | } 144 | if msg.Cmd == MSG_RT || msg.Cmd == MSG_IM || 145 | msg.Cmd == MSG_GROUP_IM || msg.Cmd == MSG_ROOM_IM { 146 | atomic.AddInt64(&client.server_summary.out_message_count, 1) 147 | } 148 | 149 | if msg.Meta != nil { 150 | meta_msg := &Message{Cmd: MSG_METADATA, Version: client.version, Body: msg.Meta} 151 | client.send(meta_msg) 152 | } 153 | client.send(msg) 154 | case messages := <-client.pwt: 155 | for _, msg := range messages { 156 | if msg.Cmd == MSG_RT || msg.Cmd == MSG_IM || 157 | msg.Cmd == MSG_GROUP_IM || msg.Cmd == MSG_ROOM_IM { 158 | atomic.AddInt64(&client.server_summary.out_message_count, 1) 159 | } 160 | 161 | if msg.Meta != nil { 162 | meta_msg := &Message{Cmd: MSG_METADATA, Version: client.version, Body: msg.Meta} 163 | client.send(meta_msg) 164 | } 165 | client.send(msg) 166 | } 167 | case <-client.lwt: 168 | client.SendMessages() 169 | } 170 | } 171 | 172 | //等待200ms,避免发送者阻塞 173 | t := time.After(200 * time.Millisecond) 174 | running = true 175 | for running { 176 | select { 177 | case <-t: 178 | running = false 179 | case <-client.wt: 180 | log.Warning("msg is dropped") 181 | } 182 | } 183 | 184 | log.Info("write goroutine exit") 185 | } 186 | 187 | func (client *Client) Run() { 188 | go client.Write() 189 | go client.Read() 190 | } 191 | -------------------------------------------------------------------------------- /server/customer_server.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "time" 24 | 25 | . "github.com/GoBelieveIO/im_service/protocol" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | func (server *Server) HandleCustomerMessageV2(client *Client, message *Message) { 30 | msg := message.Body.(*CustomerMessageV2) 31 | seq := message.Seq 32 | 33 | if client.uid == 0 { 34 | log.Warning("client has't been authenticated") 35 | return 36 | } 37 | 38 | if msg.sender != client.uid || msg.sender_appid != client.appid { 39 | log.Warningf("customer message sender:%d %d client:%d %d", 40 | msg.sender_appid, msg.sender, client.appid, client.uid) 41 | return 42 | } 43 | 44 | //限制在客服app和普通app之间 45 | if msg.sender_appid != server.kefu_appid && msg.receiver_appid != server.kefu_appid { 46 | log.Warningf("invalid appid, customer message sender:%d %d receiver:%d %d", 47 | msg.sender_appid, msg.sender, msg.receiver_appid, msg.receiver) 48 | return 49 | } 50 | 51 | log.Infof("customer message v2 sender:%d %d receiver:%d %d", 52 | msg.sender_appid, msg.sender, msg.receiver_appid, msg.receiver) 53 | 54 | msg.timestamp = int32(time.Now().Unix()) 55 | 56 | m := &Message{Cmd: MSG_CUSTOMER_V2, Version: DEFAULT_VERSION, Body: msg} 57 | 58 | msgid, prev_msgid, err := server.rpc_storage.SaveMessage(msg.receiver_appid, msg.receiver, client.device_ID, m) 59 | if err != nil { 60 | log.Warning("save customer message err:", err) 61 | return 62 | } 63 | 64 | msgid2, prev_msgid2, err := server.rpc_storage.SaveMessage(msg.sender_appid, msg.sender, client.device_ID, m) 65 | if err != nil { 66 | log.Warning("save customer message err:", err) 67 | return 68 | } 69 | 70 | server.app.PushMessage(msg.receiver_appid, msg.receiver, m) 71 | 72 | meta := &Metadata{sync_key: msgid, prev_sync_key: prev_msgid} 73 | m1 := &Message{Cmd: MSG_CUSTOMER_V2, Version: DEFAULT_VERSION, Flag: message.Flag | MESSAGE_FLAG_PUSH, Body: msg, Meta: meta} 74 | server.SendAppMessage(client, msg.receiver_appid, msg.receiver, m1) 75 | 76 | notify := &Message{Cmd: MSG_SYNC_NOTIFY, Body: &SyncKey{msgid}} 77 | server.SendAppMessage(client, msg.receiver_appid, msg.receiver, notify) 78 | 79 | //发送给自己的其它登录点 80 | meta = &Metadata{sync_key: msgid2, prev_sync_key: prev_msgid2} 81 | m2 := &Message{Cmd: MSG_CUSTOMER_V2, Version: DEFAULT_VERSION, Flag: message.Flag | MESSAGE_FLAG_PUSH, Body: msg, Meta: meta} 82 | server.SendMessage(client, client.uid, m2) 83 | 84 | notify = &Message{Cmd: MSG_SYNC_NOTIFY, Body: &SyncKey{msgid2}} 85 | server.SendMessage(client, client.uid, notify) 86 | 87 | meta = &Metadata{sync_key: msgid2, prev_sync_key: prev_msgid2} 88 | ack := &Message{Cmd: MSG_ACK, Body: &MessageACK{seq: int32(seq)}, Meta: meta} 89 | client.EnqueueMessage(ack) 90 | } 91 | -------------------------------------------------------------------------------- /server/device.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/gomodule/redigo/redis" 26 | ) 27 | 28 | func GetDeviceID(redis_pool *redis.Pool, device_id string, platform_id int) (int64, error) { 29 | conn := redis_pool.Get() 30 | defer conn.Close() 31 | key := fmt.Sprintf("devices_%s_%d", device_id, platform_id) 32 | device_ID, err := redis.Int64(conn.Do("GET", key)) 33 | if err != nil { 34 | k := "devices_id" 35 | device_ID, err = redis.Int64(conn.Do("INCR", k)) 36 | if err != nil { 37 | return 0, err 38 | } 39 | _, err = conn.Do("SET", key, device_ID) 40 | if err != nil { 41 | return 0, err 42 | } 43 | } 44 | return device_ID, err 45 | } 46 | -------------------------------------------------------------------------------- /server/group.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "database/sql" 24 | "sync" 25 | "time" 26 | 27 | mysql "github.com/go-sql-driver/mysql" 28 | 29 | log "github.com/sirupsen/logrus" 30 | ) 31 | 32 | type Group struct { 33 | gid int64 34 | appid int64 35 | super bool //超大群 36 | mutex sync.Mutex 37 | //key:成员id value:入群时间|(mute<<31) 38 | members map[int64]int64 39 | 40 | ts int //访问时间 41 | } 42 | 43 | func NewGroup(gid int64, appid int64, members map[int64]int64) *Group { 44 | group := new(Group) 45 | group.appid = appid 46 | group.gid = gid 47 | group.super = false 48 | group.members = members 49 | group.ts = int(time.Now().Unix()) 50 | return group 51 | } 52 | 53 | func NewSuperGroup(gid int64, appid int64, members map[int64]int64) *Group { 54 | group := new(Group) 55 | group.appid = appid 56 | group.gid = gid 57 | group.super = true 58 | group.members = members 59 | group.ts = int(time.Now().Unix()) 60 | return group 61 | } 62 | 63 | func (group *Group) Members() map[int64]int64 { 64 | return group.members 65 | } 66 | 67 | func (group *Group) cloneMembers() map[int64]int64 { 68 | members := group.members 69 | n := make(map[int64]int64) 70 | for k, v := range members { 71 | n[k] = v 72 | } 73 | return n 74 | } 75 | 76 | // 修改成员,在副本修改,避免读取时的lock 77 | func (group *Group) AddMember(uid int64, timestamp int) { 78 | group.mutex.Lock() 79 | defer group.mutex.Unlock() 80 | 81 | members := group.cloneMembers() 82 | members[uid] = int64(timestamp) 83 | group.members = members 84 | } 85 | 86 | func (group *Group) RemoveMember(uid int64) { 87 | group.mutex.Lock() 88 | defer group.mutex.Unlock() 89 | 90 | if _, ok := group.members[uid]; !ok { 91 | return 92 | } 93 | members := group.cloneMembers() 94 | delete(members, uid) 95 | group.members = members 96 | } 97 | 98 | func (group *Group) SetMemberMute(uid int64, mute bool) { 99 | group.mutex.Lock() 100 | defer group.mutex.Unlock() 101 | 102 | var m int64 103 | if mute { 104 | m = 1 105 | } else { 106 | m = 0 107 | } 108 | 109 | if val, ok := group.members[uid]; ok { 110 | group.members[uid] = (val & 0x7FFFFFFF) | (m << 31) 111 | } 112 | } 113 | 114 | func (group *Group) IsMember(uid int64) bool { 115 | _, ok := group.members[uid] 116 | return ok 117 | } 118 | 119 | func (group *Group) GetMemberTimestamp(uid int64) int { 120 | ts := group.members[uid] 121 | return int(ts & 0x7FFFFFFF) 122 | } 123 | 124 | func (group *Group) GetMemberMute(uid int64) bool { 125 | t := group.members[uid] 126 | return int((t>>31)&0x01) != 0 127 | } 128 | 129 | func (group *Group) IsEmpty() bool { 130 | return len(group.members) == 0 131 | } 132 | 133 | func LoadGroup(db *sql.DB, group_id int64) (*Group, error) { 134 | stmtIns, err := db.Prepare("SELECT id, appid, super FROM `group` WHERE id=? AND deleted=0") 135 | if err == mysql.ErrInvalidConn { 136 | log.Info("db prepare error:", err) 137 | stmtIns, err = db.Prepare("SELECT id, appid, super FROM `group` WHERE id=? AND deleted=0") 138 | } 139 | if err != nil { 140 | log.Info("db prepare error:", err) 141 | return nil, err 142 | } 143 | 144 | defer stmtIns.Close() 145 | 146 | var group *Group 147 | var id int64 148 | var appid int64 149 | var super int8 150 | 151 | row := stmtIns.QueryRow(group_id) 152 | err = row.Scan(&id, &appid, &super) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | members, err := LoadGroupMember(db, id) 158 | if err != nil { 159 | log.Info("error:", err) 160 | return nil, err 161 | } 162 | 163 | if super != 0 { 164 | group = NewSuperGroup(id, appid, members) 165 | } else { 166 | group = NewGroup(id, appid, members) 167 | } 168 | 169 | log.Info("load group success:", group_id) 170 | return group, nil 171 | } 172 | 173 | func LoadGroupMember(db *sql.DB, group_id int64) (map[int64]int64, error) { 174 | stmtIns, err := db.Prepare("SELECT uid, timestamp, mute FROM group_member WHERE group_id=? AND deleted=0") 175 | if err == mysql.ErrInvalidConn { 176 | log.Info("db prepare error:", err) 177 | stmtIns, err = db.Prepare("SELECT uid, timestamp, mute FROM group_member WHERE group_id=? AND deleted=0") 178 | } 179 | if err != nil { 180 | log.Info("db prepare error:", err) 181 | return nil, err 182 | } 183 | 184 | defer stmtIns.Close() 185 | members := make(map[int64]int64) 186 | rows, _ := stmtIns.Query(group_id) 187 | for rows.Next() { 188 | var uid int64 189 | var timestamp int64 190 | var mute int64 191 | rows.Scan(&uid, ×tamp, &mute) 192 | members[uid] = timestamp | (mute << 31) 193 | } 194 | return members, nil 195 | } 196 | -------------------------------------------------------------------------------- /server/group_loader.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | . "github.com/GoBelieveIO/im_service/protocol" 24 | log "github.com/sirupsen/logrus" 25 | ) 26 | 27 | type GroupLoaderRequest struct { 28 | gid int64 29 | c chan *Group 30 | } 31 | 32 | type GroupMessageDispatch struct { 33 | gid int64 34 | appid int64 35 | msg *Message 36 | } 37 | 38 | type GroupLoader struct { 39 | //保证单个群组结构只会在一个线程中被加载 40 | lt chan *GroupLoaderRequest //加载group结构到内存 41 | dt chan *GroupMessageDispatch //dispatch 群组消息 42 | 43 | group_manager *GroupManager 44 | app_route *AppRoute 45 | } 46 | 47 | func NewGroupLoader(group_manager *GroupManager, app_route *AppRoute) *GroupLoader { 48 | storage := new(GroupLoader) 49 | storage.lt = make(chan *GroupLoaderRequest) 50 | storage.dt = make(chan *GroupMessageDispatch, 1000) 51 | storage.group_manager = group_manager 52 | storage.app_route = app_route 53 | return storage 54 | } 55 | 56 | func (storage *GroupLoader) LoadGroup(gid int64) *Group { 57 | group := storage.group_manager.FindGroup(gid) 58 | if group != nil { 59 | return group 60 | } 61 | 62 | l := &GroupLoaderRequest{gid, make(chan *Group)} 63 | storage.lt <- l 64 | 65 | group = <-l.c 66 | return group 67 | } 68 | 69 | func (storage *GroupLoader) DispatchMessage(msg *Message, group_id int64, appid int64) { 70 | //assert(msg.appid > 0) 71 | group := storage.group_manager.FindGroup(group_id) 72 | if group != nil { 73 | storage.app_route.SendGroupMessage(appid, group, msg) 74 | } else { 75 | select { 76 | case storage.dt <- &GroupMessageDispatch{gid: group_id, appid: appid, msg: msg}: 77 | default: 78 | log.Warning("can't dispatch group message nonblock") 79 | } 80 | } 81 | } 82 | 83 | func (storage *GroupLoader) dispatchMessage(msg *Message, group_id int64, appid int64) { 84 | group := storage.group_manager.LoadGroup(group_id) 85 | if group == nil { 86 | log.Warning("load group nil, can't dispatch group message") 87 | return 88 | } 89 | storage.app_route.SendGroupMessage(appid, group, msg) 90 | } 91 | 92 | func (storage *GroupLoader) loadGroup(gl *GroupLoaderRequest) { 93 | group := storage.group_manager.LoadGroup(gl.gid) 94 | gl.c <- group 95 | } 96 | 97 | func (storage *GroupLoader) run() { 98 | log.Info("group message deliver running loop2") 99 | 100 | for { 101 | select { 102 | case gl := <-storage.lt: 103 | storage.loadGroup(gl) 104 | case m := <-storage.dt: 105 | storage.dispatchMessage(m.msg, m.gid, m.appid) 106 | } 107 | } 108 | } 109 | 110 | func (storage *GroupLoader) Start() { 111 | go storage.run() 112 | } 113 | -------------------------------------------------------------------------------- /server/message.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "bytes" 24 | "encoding/binary" 25 | 26 | "github.com/GoBelieveIO/im_service/protocol" 27 | ) 28 | 29 | func init() { 30 | 31 | protocol.RegisterMessageCreator(protocol.MSG_GROUP_NOTIFICATION, func() protocol.IMessage { return new(GroupNotification) }) 32 | protocol.RegisterMessageCreator(protocol.MSG_SYSTEM, func() protocol.IMessage { return new(SystemMessage) }) 33 | protocol.RegisterMessageCreator(protocol.MSG_CUSTOMER_, func() protocol.IMessage { return new(IgnoreMessage) }) 34 | protocol.RegisterMessageCreator(protocol.MSG_CUSTOMER_SUPPORT_, func() protocol.IMessage { return new(IgnoreMessage) }) 35 | protocol.RegisterMessageCreator(protocol.MSG_CUSTOMER_V2, func() protocol.IMessage { return new(CustomerMessageV2) }) 36 | 37 | protocol.RegisterMessageCreatorV(protocol.MSG_GROUP_IM, func() protocol.IVersionMessage { return new(IMMessage) }) 38 | protocol.RegisterMessageCreatorV(protocol.MSG_IM, func() protocol.IVersionMessage { return new(IMMessage) }) 39 | 40 | } 41 | 42 | // 保存在磁盘中但不再需要处理的消息 43 | type IgnoreMessage struct { 44 | } 45 | 46 | func (ignore *IgnoreMessage) ToData() []byte { 47 | return nil 48 | } 49 | 50 | func (ignore *IgnoreMessage) FromData(buff []byte) bool { 51 | return true 52 | } 53 | 54 | type GroupNotification struct { 55 | notification string 56 | } 57 | 58 | func (notification *GroupNotification) ToData() []byte { 59 | return []byte(notification.notification) 60 | } 61 | 62 | func (notification *GroupNotification) FromData(buff []byte) bool { 63 | notification.notification = string(buff) 64 | return true 65 | } 66 | 67 | type IMMessage struct { 68 | sender int64 69 | receiver int64 70 | timestamp int32 71 | msgid int32 72 | content string 73 | } 74 | 75 | func (message *IMMessage) ToDataV0() []byte { 76 | buffer := new(bytes.Buffer) 77 | binary.Write(buffer, binary.BigEndian, message.sender) 78 | binary.Write(buffer, binary.BigEndian, message.receiver) 79 | binary.Write(buffer, binary.BigEndian, message.msgid) 80 | buffer.Write([]byte(message.content)) 81 | buf := buffer.Bytes() 82 | return buf 83 | } 84 | 85 | func (im *IMMessage) FromDataV0(buff []byte) bool { 86 | if len(buff) < 20 { 87 | return false 88 | } 89 | buffer := bytes.NewBuffer(buff) 90 | binary.Read(buffer, binary.BigEndian, &im.sender) 91 | binary.Read(buffer, binary.BigEndian, &im.receiver) 92 | binary.Read(buffer, binary.BigEndian, &im.msgid) 93 | im.content = string(buff[20:]) 94 | return true 95 | } 96 | 97 | func (message *IMMessage) ToDataV1() []byte { 98 | buffer := new(bytes.Buffer) 99 | binary.Write(buffer, binary.BigEndian, message.sender) 100 | binary.Write(buffer, binary.BigEndian, message.receiver) 101 | binary.Write(buffer, binary.BigEndian, message.timestamp) 102 | binary.Write(buffer, binary.BigEndian, message.msgid) 103 | buffer.Write([]byte(message.content)) 104 | buf := buffer.Bytes() 105 | return buf 106 | } 107 | 108 | func (im *IMMessage) FromDataV1(buff []byte) bool { 109 | if len(buff) < 24 { 110 | return false 111 | } 112 | buffer := bytes.NewBuffer(buff) 113 | binary.Read(buffer, binary.BigEndian, &im.sender) 114 | binary.Read(buffer, binary.BigEndian, &im.receiver) 115 | binary.Read(buffer, binary.BigEndian, &im.timestamp) 116 | binary.Read(buffer, binary.BigEndian, &im.msgid) 117 | im.content = string(buff[24:]) 118 | return true 119 | } 120 | 121 | func (im *IMMessage) ToData(version int) []byte { 122 | if version == 0 { 123 | return im.ToDataV0() 124 | } else { 125 | return im.ToDataV1() 126 | } 127 | } 128 | 129 | func (im *IMMessage) FromData(version int, buff []byte) bool { 130 | if version == 0 { 131 | return im.FromDataV0(buff) 132 | } else { 133 | return im.FromDataV1(buff) 134 | } 135 | } 136 | 137 | func (im *IMMessage) Timestamp() int32 { 138 | return im.timestamp 139 | } 140 | 141 | type SystemMessage struct { 142 | notification string 143 | } 144 | 145 | func (sys *SystemMessage) ToData() []byte { 146 | return []byte(sys.notification) 147 | } 148 | 149 | func (sys *SystemMessage) FromData(buff []byte) bool { 150 | sys.notification = string(buff) 151 | return true 152 | } 153 | 154 | type CustomerMessageV2 struct { 155 | IMMessage 156 | sender_appid int64 157 | receiver_appid int64 158 | } 159 | 160 | func (im *CustomerMessageV2) ToData() []byte { 161 | buffer := new(bytes.Buffer) 162 | binary.Write(buffer, binary.BigEndian, im.sender_appid) 163 | binary.Write(buffer, binary.BigEndian, im.sender) 164 | binary.Write(buffer, binary.BigEndian, im.receiver_appid) 165 | binary.Write(buffer, binary.BigEndian, im.receiver) 166 | binary.Write(buffer, binary.BigEndian, im.timestamp) 167 | buffer.Write([]byte(im.content)) 168 | buf := buffer.Bytes() 169 | return buf 170 | } 171 | 172 | func (im *CustomerMessageV2) FromData(buff []byte) bool { 173 | if len(buff) < 36 { 174 | return false 175 | } 176 | buffer := bytes.NewBuffer(buff) 177 | binary.Read(buffer, binary.BigEndian, &im.sender_appid) 178 | binary.Read(buffer, binary.BigEndian, &im.sender) 179 | binary.Read(buffer, binary.BigEndian, &im.receiver_appid) 180 | binary.Read(buffer, binary.BigEndian, &im.receiver) 181 | binary.Read(buffer, binary.BigEndian, &im.timestamp) 182 | 183 | im.content = string(buff[36:]) 184 | 185 | return true 186 | } 187 | -------------------------------------------------------------------------------- /server/monitoring.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "runtime" 10 | "runtime/pprof" 11 | "strconv" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func Summary(rw http.ResponseWriter, req *http.Request, app_route *AppRoute, server_summary *ServerSummary) { 17 | m, _ := url.ParseQuery(req.URL.RawQuery) 18 | 19 | appid, _ := strconv.ParseInt(m.Get("appid"), 10, 64) 20 | 21 | obj := make(map[string]interface{}) 22 | obj["goroutine_count"] = runtime.NumGoroutine() 23 | obj["connection_count"] = server_summary.nconnections 24 | obj["client_count"] = server_summary.nclients 25 | obj["clientset_count"] = server_summary.clientset_count 26 | obj["in_message_count"] = server_summary.in_message_count 27 | obj["out_message_count"] = server_summary.out_message_count 28 | 29 | if appid != 0 { 30 | route := app_route.FindOrAddRoute(appid) 31 | clientset_count, client_count := route.GetClientCount() 32 | 33 | room_count, room_client_count, room_stat := route.GetRoomCount(1000) 34 | 35 | app_obj := make(map[string]interface{}) 36 | app_obj["clientset_count"] = clientset_count 37 | app_obj["client_count"] = client_count 38 | app_obj["room_count"] = room_count 39 | app_obj["room_client_count"] = room_client_count 40 | app_obj["room_stat"] = room_stat 41 | k := fmt.Sprintf("app_%d", appid) 42 | obj[k] = app_obj 43 | } 44 | 45 | res, err := json.Marshal(obj) 46 | if err != nil { 47 | log.Info("json marshal:", err) 48 | return 49 | } 50 | 51 | rw.Header().Add("Content-Type", "application/json") 52 | _, err = rw.Write(res) 53 | if err != nil { 54 | log.Info("write err:", err) 55 | } 56 | } 57 | 58 | func Stack(rw http.ResponseWriter, req *http.Request) { 59 | pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 60 | rw.WriteHeader(200) 61 | } 62 | 63 | func WriteHttpError(status int, err string, w http.ResponseWriter) { 64 | w.Header().Set("Content-Type", "application/json") 65 | obj := make(map[string]interface{}) 66 | meta := make(map[string]interface{}) 67 | meta["code"] = status 68 | meta["message"] = err 69 | obj["meta"] = meta 70 | b, _ := json.Marshal(obj) 71 | w.WriteHeader(status) 72 | w.Write(b) 73 | } 74 | 75 | func WriteHttpObj(data map[string]interface{}, w http.ResponseWriter) { 76 | w.Header().Set("Content-Type", "application/json") 77 | obj := make(map[string]interface{}) 78 | obj["data"] = data 79 | b, _ := json.Marshal(obj) 80 | w.Write(b) 81 | } 82 | -------------------------------------------------------------------------------- /server/push_service.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "encoding/json" 24 | "fmt" 25 | "time" 26 | 27 | "github.com/gomodule/redigo/redis" 28 | log "github.com/sirupsen/logrus" 29 | ) 30 | 31 | const PUSH_QUEUE_TIMEOUT = 300 32 | 33 | type Push struct { 34 | queue_name string 35 | content []byte 36 | } 37 | 38 | type PushService struct { 39 | pwt chan *Push 40 | redis_pool *redis.Pool 41 | } 42 | 43 | func NewPushService(redis_pool *redis.Pool) *PushService { 44 | s := &PushService{} 45 | s.pwt = make(chan *Push, 10000) 46 | s.redis_pool = redis_pool 47 | return s 48 | } 49 | 50 | func (push_service *PushService) Run() { 51 | go push_service.Push() 52 | } 53 | 54 | func (push_service *PushService) IsROMApp(appid int64) bool { 55 | return false 56 | } 57 | 58 | // 离线消息入apns队列 59 | func (push_service *PushService) PublishPeerMessage(appid int64, im *IMMessage) { 60 | v := make(map[string]interface{}) 61 | v["appid"] = appid 62 | v["sender"] = im.sender 63 | v["receiver"] = im.receiver 64 | v["content"] = im.content 65 | 66 | b, _ := json.Marshal(v) 67 | var queue_name string 68 | if push_service.IsROMApp(appid) { 69 | queue_name = fmt.Sprintf("push_queue_%d", appid) 70 | } else { 71 | queue_name = "push_queue" 72 | } 73 | 74 | push_service.pushChan(queue_name, b) 75 | } 76 | 77 | func (push_service *PushService) PublishCustomerMessageV2(appid int64, im *CustomerMessageV2) { 78 | v := make(map[string]interface{}) 79 | v["appid"] = appid 80 | v["sender_appid"] = im.sender_appid 81 | v["sender"] = im.sender 82 | v["receiver_appid"] = im.receiver_appid 83 | v["receiver"] = im.receiver 84 | v["content"] = im.content 85 | 86 | b, _ := json.Marshal(v) 87 | var queue_name string 88 | if push_service.IsROMApp(appid) { 89 | queue_name = fmt.Sprintf("customer_push_queue_v2_%d", appid) 90 | } else { 91 | queue_name = "customer_push_queue_v2" 92 | } 93 | 94 | push_service.pushChan(queue_name, b) 95 | } 96 | 97 | func (push_service *PushService) PublishGroupMessage(appid int64, receivers []int64, im *IMMessage) { 98 | v := make(map[string]interface{}) 99 | v["appid"] = appid 100 | v["sender"] = im.sender 101 | v["receivers"] = receivers 102 | v["content"] = im.content 103 | v["group_id"] = im.receiver 104 | 105 | b, _ := json.Marshal(v) 106 | var queue_name string 107 | if push_service.IsROMApp(appid) { 108 | queue_name = fmt.Sprintf("group_push_queue_%d", appid) 109 | } else { 110 | queue_name = "group_push_queue" 111 | } 112 | 113 | push_service.pushChan(queue_name, b) 114 | } 115 | 116 | func (push_service *PushService) PublishSystemMessage(appid, receiver int64, sys *SystemMessage) { 117 | content := sys.notification 118 | v := make(map[string]interface{}) 119 | v["appid"] = appid 120 | v["receiver"] = receiver 121 | v["content"] = content 122 | 123 | b, _ := json.Marshal(v) 124 | queue_name := "system_push_queue" 125 | 126 | push_service.pushChan(queue_name, b) 127 | } 128 | 129 | func (push_service *PushService) pushChan(queue_name string, b []byte) { 130 | select { 131 | case push_service.pwt <- &Push{queue_name, b}: 132 | default: 133 | log.Warning("rpush message timeout") 134 | } 135 | } 136 | 137 | func (push_service *PushService) PushQueue(ps []*Push) { 138 | conn := push_service.redis_pool.Get() 139 | defer conn.Close() 140 | 141 | begin := time.Now() 142 | conn.Send("MULTI") 143 | for _, p := range ps { 144 | conn.Send("RPUSH", p.queue_name, p.content) 145 | } 146 | _, err := conn.Do("EXEC") 147 | 148 | end := time.Now() 149 | duration := end.Sub(begin) 150 | if err != nil { 151 | log.Info("multi rpush error:", err) 152 | } else { 153 | log.Infof("mmulti rpush:%d time:%s success", len(ps), duration) 154 | } 155 | 156 | if duration > time.Millisecond*PUSH_QUEUE_TIMEOUT { 157 | log.Warning("multi rpush slow:", duration) 158 | } 159 | } 160 | 161 | func (push_service *PushService) Push() { 162 | //单次入redis队列消息限制 163 | const PUSH_LIMIT = 1000 164 | const WAIT_TIMEOUT = 500 165 | 166 | closed := false 167 | ps := make([]*Push, 0, PUSH_LIMIT) 168 | for !closed { 169 | ps = ps[:0] 170 | //blocking for first message 171 | p := <-push_service.pwt 172 | if p == nil { 173 | closed = true 174 | break 175 | } 176 | ps = append(ps, p) 177 | 178 | //non blocking 179 | Loop1: 180 | for !closed { 181 | select { 182 | case p := <-push_service.pwt: 183 | if p == nil { 184 | closed = true 185 | } else { 186 | ps = append(ps, p) 187 | if len(ps) >= PUSH_LIMIT { 188 | break Loop1 189 | } 190 | } 191 | default: 192 | break Loop1 193 | } 194 | } 195 | 196 | if closed { 197 | push_service.PushQueue(ps) 198 | return 199 | } 200 | 201 | if len(ps) >= PUSH_LIMIT { 202 | push_service.PushQueue(ps) 203 | continue 204 | } 205 | 206 | //blocking with timeout 207 | begin := time.Now() 208 | end := begin.Add(time.Millisecond * WAIT_TIMEOUT) 209 | Loop2: 210 | for !closed { 211 | now := time.Now() 212 | if !end.After(now) { 213 | break 214 | } 215 | d := end.Sub(now) 216 | select { 217 | case p := <-push_service.pwt: 218 | if p == nil { 219 | closed = true 220 | } else { 221 | ps = append(ps, p) 222 | if len(ps) >= PUSH_LIMIT { 223 | break Loop2 224 | } 225 | } 226 | case <-time.After(d): 227 | break Loop2 228 | } 229 | } 230 | 231 | if len(ps) > 0 { 232 | push_service.PushQueue(ps) 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /server/redis_config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | type RedisConfig struct { 23 | redis_address string 24 | redis_password string 25 | redis_db int 26 | } 27 | 28 | func NewRedisConfig(addr string, passwd string, db int) *RedisConfig { 29 | c := &RedisConfig{ 30 | redis_address: addr, 31 | redis_password: passwd, 32 | redis_db: db, 33 | } 34 | return c 35 | } 36 | -------------------------------------------------------------------------------- /server/relationship.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "database/sql" 24 | 25 | mysql "github.com/go-sql-driver/mysql" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | const NoneRelationship = 0 30 | 31 | type Relationship int32 32 | 33 | func NewRelationship(is_my_friend bool, is_your_friend bool, is_in_my_blacklist bool, is_in_your_blacklist bool) Relationship { 34 | var r Relationship 35 | 36 | if is_my_friend { 37 | r |= 0x01 38 | } 39 | if is_your_friend { 40 | r |= 0x02 41 | } 42 | if is_in_my_blacklist { 43 | r |= 0x04 44 | } 45 | if is_in_your_blacklist { 46 | r |= 0x08 47 | } 48 | return r 49 | } 50 | 51 | func (rs Relationship) IsMyFriend() bool { 52 | return (rs & 0x01) != 0 53 | } 54 | 55 | func (rs Relationship) IsYourFriend() bool { 56 | return (rs & 0x02) != 0 57 | } 58 | 59 | func (rs Relationship) IsInMyBlacklist() bool { 60 | return (rs & 0x04) != 0 61 | } 62 | 63 | func (rs Relationship) IsInYourBlacklist() bool { 64 | return (rs & 0x08) != 0 65 | } 66 | 67 | func (rs Relationship) reverse() Relationship { 68 | return NewRelationship(rs.IsYourFriend(), rs.IsMyFriend(), rs.IsInYourBlacklist(), rs.IsInMyBlacklist()) 69 | } 70 | 71 | func GetRelationship(db *sql.DB, appid int64, uid int64, friend_uid int64) (Relationship, error) { 72 | stmtIns, err := db.Prepare("SELECT uid, friend_uid FROM `friend` WHERE appid=? AND uid=? AND friend_uid=?") 73 | if err == mysql.ErrInvalidConn { 74 | log.Info("db prepare error:", err) 75 | stmtIns, err = db.Prepare("SELECT uid, friend_uid FROM `friend` WHERE appid=? AND uid=? AND friend_uid=?") 76 | } 77 | 78 | if err != nil { 79 | log.Info("db prepare error:", err) 80 | return NoneRelationship, err 81 | } 82 | 83 | var is_my_friend, is_your_friend, is_in_my_blacklist, is_in_your_blacklist bool 84 | var _uid int64 85 | var _friend_uid int64 86 | 87 | row := stmtIns.QueryRow(appid, uid, friend_uid) 88 | err = row.Scan(&_uid, &_friend_uid) 89 | if err != nil { 90 | log.Info("scan error:", err) 91 | } 92 | is_my_friend = err == nil 93 | 94 | row = stmtIns.QueryRow(appid, friend_uid, uid) 95 | err = row.Scan(&_uid, &_friend_uid) 96 | if err != nil { 97 | log.Info("scan error:", err) 98 | } 99 | is_your_friend = err == nil 100 | 101 | stmtIns.Close() 102 | 103 | stmtIns, err = db.Prepare("SELECT uid, friend_uid FROM `blacklist` WHERE appid=? AND uid=? AND friend_uid=?") 104 | if err != nil { 105 | log.Info("error:", err) 106 | return NoneRelationship, err 107 | } 108 | 109 | row = stmtIns.QueryRow(appid, uid, friend_uid) 110 | err = row.Scan(&_uid, &_friend_uid) 111 | if err != nil { 112 | log.Info("scan error:", err) 113 | } 114 | is_in_my_blacklist = err == nil 115 | 116 | row = stmtIns.QueryRow(appid, friend_uid, uid) 117 | err = row.Scan(&_uid, &_friend_uid) 118 | if err != nil { 119 | log.Info("scan error:", err) 120 | } 121 | is_in_your_blacklist = err == nil 122 | 123 | stmtIns.Close() 124 | 125 | rs := NewRelationship(is_my_friend, is_your_friend, is_in_my_blacklist, is_in_your_blacklist) 126 | 127 | return rs, nil 128 | } 129 | -------------------------------------------------------------------------------- /server/room_server.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "sync/atomic" 24 | 25 | log "github.com/sirupsen/logrus" 26 | 27 | . "github.com/GoBelieveIO/im_service/protocol" 28 | ) 29 | 30 | func (server *Server) HandleEnterRoom(client *Client, msg *Message) { 31 | room := msg.Body.(*Room) 32 | 33 | if client.uid == 0 { 34 | log.Warning("client has't been authenticated") 35 | return 36 | } 37 | 38 | room_id := room.RoomID() 39 | log.Info("enter room id:", room_id) 40 | if room_id == 0 || client.room_id == room_id { 41 | return 42 | } 43 | route := server.app_route.FindOrAddRoute(client.appid) 44 | if client.room_id > 0 { 45 | channel := server.app.GetRoomChannel(client.room_id) 46 | channel.UnsubscribeRoom(client.appid, client.room_id) 47 | 48 | route.RemoveRoomClient(client.room_id, client.Client()) 49 | } 50 | 51 | client.room_id = room_id 52 | route.AddRoomClient(client.room_id, client.Client()) 53 | channel := server.app.GetRoomChannel(client.room_id) 54 | channel.SubscribeRoom(client.appid, client.room_id) 55 | } 56 | 57 | func (server *Server) HandleLeaveRoom(client *Client, msg *Message) { 58 | room := msg.Body.(*Room) 59 | if client.uid == 0 { 60 | log.Warning("client has't been authenticated") 61 | return 62 | } 63 | 64 | room_id := room.RoomID() 65 | log.Info("leave room id:", room_id) 66 | if room_id == 0 { 67 | return 68 | } 69 | if client.room_id != room_id { 70 | return 71 | } 72 | 73 | route := server.app_route.FindOrAddRoute(client.appid) 74 | route.RemoveRoomClient(client.room_id, client.Client()) 75 | channel := server.app.GetRoomChannel(client.room_id) 76 | channel.UnsubscribeRoom(client.appid, client.room_id) 77 | client.room_id = 0 78 | } 79 | 80 | func (server *Server) HandleRoomIM(client *Client, msg *Message) { 81 | room_im := msg.Body.(*RoomMessage) 82 | seq := msg.Seq 83 | 84 | if client.uid == 0 { 85 | log.Warning("client has't been authenticated") 86 | return 87 | } 88 | room_id := room_im.receiver 89 | if room_id != client.room_id { 90 | log.Warningf("room id:%d is't client's room id:%d\n", room_id, client.room_id) 91 | return 92 | } 93 | 94 | fb := atomic.LoadInt32(&client.forbidden) 95 | if fb == 1 { 96 | log.Infof("room id:%d client:%d, %d is forbidden", room_id, client.appid, client.uid) 97 | return 98 | } 99 | 100 | m := &Message{Cmd: MSG_ROOM_IM, Body: room_im, BodyData: room_im.ToData()} 101 | 102 | server.SendRoomMessage(client, room_id, m) 103 | 104 | ack := &Message{Cmd: MSG_ACK, Body: &MessageACK{seq: int32(seq)}} 105 | r := client.EnqueueMessage(ack) 106 | if !r { 107 | log.Warning("send room message ack error") 108 | } 109 | 110 | atomic.AddInt64(&server.server_summary.in_message_count, 1) 111 | } 112 | -------------------------------------------------------------------------------- /server/route.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/GoBelieveIO/im_service/set" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // 一个聊天室中不允许有多个相同uid的client 11 | const ROOM_SINGLE = false 12 | 13 | // 一个聊天室中不允许有多个相同uid和deviceid的client 14 | const ROOM_DEVICE_SINGLE = true 15 | 16 | type ClientSet = set.Set[*Client] 17 | 18 | func NewClientSet() ClientSet { 19 | return set.NewSet[*Client]() 20 | } 21 | 22 | type Route struct { 23 | appid int64 24 | mutex sync.Mutex 25 | clients map[int64]ClientSet 26 | room_clients map[int64]ClientSet 27 | } 28 | 29 | func NewRoute(appid int64) *Route { 30 | route := new(Route) 31 | route.appid = appid 32 | route.clients = make(map[int64]ClientSet) 33 | route.room_clients = make(map[int64]ClientSet) 34 | return route 35 | } 36 | 37 | func (route *Route) GetRoomCount(low_level int) (int, int, map[int64]int) { 38 | route.mutex.Lock() 39 | defer route.mutex.Unlock() 40 | stat := make(map[int64]int) 41 | count := 0 42 | for k, v := range route.room_clients { 43 | if len(v) >= low_level { 44 | stat[k] = len(v) 45 | } 46 | count += len(v) 47 | } 48 | return len(route.room_clients), count, stat 49 | } 50 | 51 | func (route *Route) AddRoomClient(room_id int64, client *Client) { 52 | route.mutex.Lock() 53 | defer route.mutex.Unlock() 54 | set, ok := route.room_clients[room_id] 55 | if !ok { 56 | set = NewClientSet() 57 | route.room_clients[room_id] = set 58 | } 59 | 60 | if ROOM_SINGLE { 61 | var old *Client = nil 62 | for k := range set { 63 | //根据uid去重 64 | if k.uid == client.uid { 65 | old = k 66 | break 67 | } 68 | } 69 | if old != nil { 70 | set.Remove(old) 71 | } 72 | } else if ROOM_DEVICE_SINGLE && client.device_ID > 0 { 73 | var old *Client = nil 74 | for k := range set { 75 | //根据uid&device_id去重 76 | if k.uid == client.uid && k.device_ID == client.device_ID { 77 | old = k 78 | break 79 | } 80 | } 81 | if old != nil { 82 | set.Remove(old) 83 | } 84 | } 85 | set.Add(client) 86 | } 87 | 88 | // todo optimise client set clone 89 | func (route *Route) FindRoomClientSet(room_id int64) ClientSet { 90 | route.mutex.Lock() 91 | defer route.mutex.Unlock() 92 | 93 | set, ok := route.room_clients[room_id] 94 | if ok { 95 | return set.Clone() 96 | } else { 97 | return nil 98 | } 99 | } 100 | 101 | func (route *Route) RemoveRoomClient(room_id int64, client *Client) bool { 102 | route.mutex.Lock() 103 | defer route.mutex.Unlock() 104 | if set, ok := route.room_clients[room_id]; ok { 105 | set.Remove(client) 106 | if set.Count() == 0 { 107 | delete(route.room_clients, room_id) 108 | } 109 | return true 110 | } 111 | log.Info("room client non exists") 112 | return false 113 | } 114 | 115 | func (route *Route) GetClientCount() (int, int) { 116 | route.mutex.Lock() 117 | defer route.mutex.Unlock() 118 | 119 | count := 0 120 | for _, v := range route.clients { 121 | count += len(v) 122 | } 123 | return len(route.clients), count 124 | } 125 | 126 | func (route *Route) AddClient(client *Client) bool { 127 | is_new := false 128 | route.mutex.Lock() 129 | defer route.mutex.Unlock() 130 | set, ok := route.clients[client.uid] 131 | if !ok { 132 | set = NewClientSet() 133 | route.clients[client.uid] = set 134 | is_new = true 135 | } 136 | set.Add(client) 137 | return is_new 138 | } 139 | 140 | func (route *Route) RemoveClient(client *Client) (bool, bool) { 141 | route.mutex.Lock() 142 | defer route.mutex.Unlock() 143 | if set, ok := route.clients[client.uid]; ok { 144 | is_delete := false 145 | set.Remove(client) 146 | if set.Count() == 0 { 147 | delete(route.clients, client.uid) 148 | is_delete = true 149 | } 150 | return true, is_delete 151 | } 152 | log.Info("client non exists") 153 | return false, false 154 | } 155 | 156 | func (route *Route) FindClientSet(uid int64) ClientSet { 157 | route.mutex.Lock() 158 | defer route.mutex.Unlock() 159 | 160 | set, ok := route.clients[uid] 161 | if ok { 162 | return set.Clone() 163 | } else { 164 | return nil 165 | } 166 | } 167 | 168 | func (route *Route) IsOnline(uid int64) bool { 169 | route.mutex.Lock() 170 | defer route.mutex.Unlock() 171 | 172 | set, ok := route.clients[uid] 173 | if ok { 174 | return len(set) > 0 175 | } 176 | return false 177 | } 178 | 179 | func (route *Route) GetUserIDs() set.IntSet { 180 | return set.NewIntSet() 181 | } 182 | -------------------------------------------------------------------------------- /server/server_summary.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type ServerSummary struct { 4 | nconnections int64 5 | nclients int64 6 | clientset_count int64 //重复uid的client对象不计数 7 | in_message_count int64 8 | out_message_count int64 9 | } 10 | 11 | func NewServerSummary() *ServerSummary { 12 | s := new(ServerSummary) 13 | return s 14 | } 15 | -------------------------------------------------------------------------------- /server/subscriber.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "strconv" 24 | "strings" 25 | "sync/atomic" 26 | "time" 27 | 28 | "github.com/gomodule/redigo/redis" 29 | 30 | log "github.com/sirupsen/logrus" 31 | ) 32 | 33 | func HandleForbidden(data string, app_route *AppRoute) { 34 | arr := strings.Split(data, ",") 35 | if len(arr) != 3 { 36 | log.Info("message error:", data) 37 | return 38 | } 39 | appid, err := strconv.ParseInt(arr[0], 10, 64) 40 | if err != nil { 41 | log.Info("error:", err) 42 | return 43 | } 44 | uid, err := strconv.ParseInt(arr[1], 10, 64) 45 | if err != nil { 46 | log.Info("error:", err) 47 | return 48 | } 49 | fb, err := strconv.ParseInt(arr[2], 10, 64) 50 | if err != nil { 51 | log.Info("error:", err) 52 | return 53 | } 54 | 55 | route := app_route.FindRoute(appid) 56 | if route == nil { 57 | log.Warningf("can't find appid:%d route", appid) 58 | return 59 | } 60 | clients := route.FindClientSet(uid) 61 | if len(clients) == 0 { 62 | return 63 | } 64 | 65 | log.Infof("forbidden:%d %d %d client count:%d", 66 | appid, uid, fb, len(clients)) 67 | for c := range clients { 68 | atomic.StoreInt32(&c.forbidden, int32(fb)) 69 | } 70 | } 71 | 72 | func SubscribeRedis(app_route *AppRoute, config *RedisConfig) bool { 73 | c, err := redis.Dial("tcp", config.redis_address) 74 | if err != nil { 75 | log.Info("dial redis error:", err) 76 | return false 77 | } 78 | 79 | password := config.redis_password 80 | if len(password) > 0 { 81 | if _, err := c.Do("AUTH", password); err != nil { 82 | c.Close() 83 | return false 84 | } 85 | } 86 | 87 | psc := redis.PubSubConn{Conn: c} 88 | psc.Subscribe("speak_forbidden") 89 | 90 | for { 91 | switch v := psc.Receive().(type) { 92 | case redis.Message: 93 | log.Infof("%s: message: %s\n", v.Channel, v.Data) 94 | if v.Channel == "speak_forbidden" { 95 | HandleForbidden(string(v.Data), app_route) 96 | } 97 | case redis.Subscription: 98 | log.Infof("%s: %s %d\n", v.Channel, v.Kind, v.Count) 99 | case error: 100 | log.Info("error:", v) 101 | return true 102 | } 103 | } 104 | } 105 | 106 | func ListenRedis(app_route *AppRoute, config *RedisConfig) { 107 | nsleep := 1 108 | for { 109 | connected := SubscribeRedis(app_route, config) 110 | if !connected { 111 | nsleep *= 2 112 | if nsleep > 60 { 113 | nsleep = 60 114 | } 115 | } else { 116 | nsleep = 1 117 | } 118 | time.Sleep(time.Duration(nsleep) * time.Second) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /server/user.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package server 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/gomodule/redigo/redis" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | func GetSyncKey(redis_pool *redis.Pool, appid int64, uid int64) int64 { 30 | conn := redis_pool.Get() 31 | defer conn.Close() 32 | 33 | key := fmt.Sprintf("users_%d_%d", appid, uid) 34 | 35 | origin, err := redis.Int64(conn.Do("HGET", key, "sync_key")) 36 | if err != nil && err != redis.ErrNil { 37 | log.Info("hget error:", err) 38 | return 0 39 | } 40 | return origin 41 | } 42 | 43 | func GetGroupSyncKey(redis_pool *redis.Pool, appid int64, uid int64, group_id int64) int64 { 44 | conn := redis_pool.Get() 45 | defer conn.Close() 46 | 47 | key := fmt.Sprintf("users_%d_%d", appid, uid) 48 | field := fmt.Sprintf("group_sync_key_%d", group_id) 49 | 50 | origin, err := redis.Int64(conn.Do("HGET", key, field)) 51 | if err != nil && err != redis.ErrNil { 52 | log.Info("hget error:", err) 53 | return 0 54 | } 55 | return origin 56 | } 57 | 58 | func SaveSyncKey(redis_pool *redis.Pool, appid int64, uid int64, sync_key int64) { 59 | conn := redis_pool.Get() 60 | defer conn.Close() 61 | 62 | key := fmt.Sprintf("users_%d_%d", appid, uid) 63 | 64 | _, err := conn.Do("HSET", key, "sync_key", sync_key) 65 | if err != nil { 66 | log.Warning("hset error:", err) 67 | } 68 | } 69 | 70 | func SaveGroupSyncKey(redis_pool *redis.Pool, appid int64, uid int64, group_id int64, sync_key int64) { 71 | conn := redis_pool.Get() 72 | defer conn.Close() 73 | 74 | key := fmt.Sprintf("users_%d_%d", appid, uid) 75 | field := fmt.Sprintf("group_sync_key_%d", group_id) 76 | 77 | _, err := conn.Do("HSET", key, field, sync_key) 78 | if err != nil { 79 | log.Warning("hset error:", err) 80 | } 81 | } 82 | 83 | func GetUserPreferences(redis_pool *redis.Pool, appid int64, uid int64) (int, bool, error) { 84 | conn := redis_pool.Get() 85 | defer conn.Close() 86 | 87 | key := fmt.Sprintf("users_%d_%d", appid, uid) 88 | 89 | reply, err := redis.Values(conn.Do("HMGET", key, "forbidden", "notification_on")) 90 | if err != nil { 91 | log.Info("hget error:", err) 92 | return 0, false, err 93 | } 94 | 95 | //电脑在线,手机新消息通知 96 | var notification_on int 97 | //用户禁言 98 | var forbidden int 99 | _, err = redis.Scan(reply, &forbidden, ¬ification_on) 100 | if err != nil { 101 | log.Warning("scan error:", err) 102 | return 0, false, err 103 | } 104 | 105 | return forbidden, notification_on != 0, nil 106 | } 107 | 108 | func SetUserUnreadCount(redis_pool *redis.Pool, appid int64, uid int64, count int32) { 109 | conn := redis_pool.Get() 110 | defer conn.Close() 111 | 112 | key := fmt.Sprintf("users_%d_%d", appid, uid) 113 | _, err := conn.Do("HSET", key, "unread", count) 114 | if err != nil { 115 | log.Info("hset err:", err) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /set/set.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package set 21 | 22 | type Set[T comparable] map[T]struct{} 23 | 24 | func NewSet[T comparable]() Set[T] { 25 | return make(map[T]struct{}) 26 | } 27 | 28 | func (set Set[T]) Add(v T) { 29 | if _, ok := set[v]; ok { 30 | return 31 | } 32 | set[v] = struct{}{} 33 | } 34 | 35 | func (set Set[T]) IsMember(v T) bool { 36 | if _, ok := set[v]; ok { 37 | return true 38 | } 39 | return false 40 | } 41 | 42 | func (set Set[T]) Remove(v T) { 43 | if _, ok := set[v]; !ok { 44 | return 45 | } 46 | delete(set, v) 47 | } 48 | 49 | func (set Set[T]) Clone() Set[T] { 50 | n := make(map[T]struct{}) 51 | for k, v := range set { 52 | n[k] = v 53 | } 54 | return n 55 | } 56 | 57 | func (set Set[T]) Count() int { 58 | return len(set) 59 | } 60 | 61 | type IntSet = Set[int64] 62 | 63 | func NewIntSet() IntSet { 64 | return NewSet[int64]() 65 | } 66 | -------------------------------------------------------------------------------- /storage/master.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package storage 21 | 22 | import ( 23 | "sync" 24 | "time" 25 | 26 | . "github.com/GoBelieveIO/im_service/protocol" 27 | ) 28 | 29 | type Master struct { 30 | ewt chan *EMessage 31 | 32 | mutex sync.Mutex 33 | clients map[*SyncClient]struct{} 34 | } 35 | 36 | func NewMaster() *Master { 37 | master := new(Master) 38 | master.clients = make(map[*SyncClient]struct{}) 39 | master.ewt = make(chan *EMessage, 10) 40 | return master 41 | } 42 | 43 | func (master *Master) Channel() chan *EMessage { 44 | return master.ewt 45 | } 46 | 47 | func (master *Master) AddClient(client *SyncClient) { 48 | master.mutex.Lock() 49 | defer master.mutex.Unlock() 50 | master.clients[client] = struct{}{} 51 | } 52 | 53 | func (master *Master) RemoveClient(client *SyncClient) { 54 | master.mutex.Lock() 55 | defer master.mutex.Unlock() 56 | delete(master.clients, client) 57 | } 58 | 59 | func (master *Master) CloneClientSet() map[*SyncClient]struct{} { 60 | master.mutex.Lock() 61 | defer master.mutex.Unlock() 62 | clone := make(map[*SyncClient]struct{}) 63 | for k, v := range master.clients { 64 | clone[k] = v 65 | } 66 | return clone 67 | } 68 | 69 | func (master *Master) SendBatch(cache []*EMessage) { 70 | if len(cache) == 0 { 71 | return 72 | } 73 | 74 | batch := &MessageBatch{msgs: make([]*Message, 0, 1000)} 75 | batch.first_id = cache[0].MsgId 76 | for _, em := range cache { 77 | batch.last_id = em.MsgId 78 | batch.msgs = append(batch.msgs, em.Msg) 79 | } 80 | m := &Message{Cmd: MSG_STORAGE_SYNC_MESSAGE_BATCH, Body: batch} 81 | clients := master.CloneClientSet() 82 | for c := range clients { 83 | c.ewt <- m 84 | } 85 | } 86 | 87 | func (master *Master) Run() { 88 | cache := make([]*EMessage, 0, 1000) 89 | var first_ts time.Time 90 | for { 91 | t := 60 * time.Second 92 | if len(cache) > 0 { 93 | ts := first_ts.Add(time.Second * 1) 94 | now := time.Now() 95 | 96 | if ts.After(now) { 97 | t = ts.Sub(now) 98 | } else { 99 | master.SendBatch(cache) 100 | cache = cache[0:0] 101 | } 102 | } 103 | select { 104 | case emsg := <-master.ewt: 105 | cache = append(cache, emsg) 106 | if len(cache) == 1 { 107 | first_ts = time.Now() 108 | } 109 | if len(cache) >= 1000 { 110 | master.SendBatch(cache) 111 | cache = cache[0:0] 112 | } 113 | case <-time.After(t): 114 | if len(cache) > 0 { 115 | master.SendBatch(cache) 116 | cache = cache[0:0] 117 | } 118 | } 119 | } 120 | } 121 | 122 | func (master *Master) Start() { 123 | go master.Run() 124 | } 125 | -------------------------------------------------------------------------------- /storage/rpc.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package storage 21 | 22 | type PeerMessage struct { 23 | AppID int64 24 | Uid int64 25 | DeviceID int64 26 | Cmd int32 27 | Raw []byte 28 | } 29 | 30 | type PeerGroupMessage struct { 31 | AppID int64 32 | Members []int64 33 | DeviceID int64 34 | Cmd int32 35 | Raw []byte 36 | } 37 | 38 | 39 | type GroupMessage struct { 40 | AppID int64 41 | GroupID int64 42 | DeviceID int64 43 | Cmd int32 44 | Raw []byte 45 | } 46 | 47 | 48 | type SyncHistory struct { 49 | AppID int64 50 | Uid int64 51 | DeviceID int64 52 | LastMsgID int64 53 | } 54 | 55 | type SyncGroupHistory struct { 56 | AppID int64 57 | Uid int64 58 | DeviceID int64 59 | GroupID int64 60 | LastMsgID int64 61 | Timestamp int32 62 | } 63 | 64 | type HistoryRequest struct { 65 | AppID int64 66 | Uid int64 67 | Limit int32 68 | } 69 | 70 | 71 | type HistoryMessageID struct { 72 | MsgID int64 73 | PrevMsgID int64 74 | } 75 | 76 | type GroupHistoryMessageID struct { 77 | MessageIDs []*HistoryMessageID 78 | } 79 | 80 | type HistoryMessage struct { 81 | MsgID int64 82 | DeviceID int64 //消息发送者所在的设备ID 83 | Cmd int32 84 | Raw []byte 85 | } 86 | 87 | type PeerHistoryMessage struct { 88 | Messages []*HistoryMessage 89 | LastMsgID int64 90 | HasMore bool 91 | } 92 | 93 | type GroupHistoryMessage PeerHistoryMessage 94 | 95 | type LatestMessage struct { 96 | Messages []*HistoryMessage 97 | } 98 | 99 | 100 | type RPCStorage interface { 101 | SyncMessage(sync_key *SyncHistory, result *PeerHistoryMessage) error 102 | 103 | SyncGroupMessage(sync_key *SyncGroupHistory, result *GroupHistoryMessage) error 104 | 105 | SavePeerMessage(m *PeerMessage, result *HistoryMessageID) error 106 | 107 | SavePeerGroupMessage(m *PeerGroupMessage, result *GroupHistoryMessageID) error 108 | 109 | SaveGroupMessage(m *GroupMessage, result *HistoryMessageID) error 110 | 111 | GetNewCount(sync_key *SyncHistory, new_count *int64) error 112 | 113 | GetLatestMessage(r *HistoryRequest, l *LatestMessage) error 114 | 115 | Ping(int, *int) error 116 | } 117 | -------------------------------------------------------------------------------- /storage/slaver.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package storage 21 | 22 | import ( 23 | "net" 24 | "time" 25 | 26 | log "github.com/sirupsen/logrus" 27 | 28 | . "github.com/GoBelieveIO/im_service/protocol" 29 | ) 30 | 31 | type Slaver struct { 32 | addr string 33 | storage *Storage 34 | } 35 | 36 | func NewSlaver(addr string, storage *Storage) *Slaver { 37 | s := new(Slaver) 38 | s.addr = addr 39 | s.storage = storage 40 | return s 41 | } 42 | 43 | func (slaver *Slaver) RunOnce(conn *net.TCPConn) { 44 | defer conn.Close() 45 | 46 | seq := 0 47 | 48 | msgid := slaver.storage.NextMessageID() 49 | cursor := &SyncCursor{msgid} 50 | log.Info("cursor msgid:", msgid) 51 | 52 | msg := &Message{Cmd: MSG_STORAGE_SYNC_BEGIN, Body: cursor} 53 | seq += 1 54 | msg.Seq = seq 55 | SendMessage(conn, msg) 56 | 57 | for { 58 | msg := ReceiveStorageSyncMessage(conn) 59 | if msg == nil { 60 | return 61 | } 62 | 63 | if msg.Cmd == MSG_STORAGE_SYNC_MESSAGE { 64 | emsg := msg.Body.(*EMessage) 65 | slaver.storage.SaveSyncMessage(emsg) 66 | } else if msg.Cmd == MSG_STORAGE_SYNC_MESSAGE_BATCH { 67 | mb := msg.Body.(*MessageBatch) 68 | slaver.storage.SaveSyncMessageBatch(mb) 69 | } else { 70 | log.Error("unknown message cmd:", Command(msg.Cmd)) 71 | } 72 | } 73 | } 74 | 75 | func (slaver *Slaver) Run() { 76 | nsleep := 100 77 | for { 78 | conn, err := net.Dial("tcp", slaver.addr) 79 | if err != nil { 80 | log.Info("connect master server error:", err) 81 | nsleep *= 2 82 | if nsleep > 60*1000 { 83 | nsleep = 60 * 1000 84 | } 85 | log.Info("slaver sleep:", nsleep) 86 | time.Sleep(time.Duration(nsleep) * time.Millisecond) 87 | continue 88 | } 89 | tconn := conn.(*net.TCPConn) 90 | tconn.SetKeepAlive(true) 91 | tconn.SetKeepAlivePeriod(time.Duration(10 * 60 * time.Second)) 92 | log.Info("slaver connected with master") 93 | nsleep = 100 94 | slaver.RunOnce(tconn) 95 | } 96 | } 97 | 98 | func (slaver *Slaver) Start() { 99 | go slaver.Run() 100 | } 101 | -------------------------------------------------------------------------------- /storage/sync_client.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package storage 21 | 22 | import ( 23 | "net" 24 | 25 | log "github.com/sirupsen/logrus" 26 | 27 | . "github.com/GoBelieveIO/im_service/protocol" 28 | ) 29 | 30 | type SyncClient struct { 31 | conn *net.TCPConn 32 | ewt chan *Message 33 | storage *Storage 34 | master *Master 35 | } 36 | 37 | func NewSyncClient(conn *net.TCPConn, storage *Storage, master *Master) *SyncClient { 38 | c := new(SyncClient) 39 | c.conn = conn 40 | c.ewt = make(chan *Message, 10) 41 | c.storage = storage 42 | c.master = master 43 | return c 44 | } 45 | 46 | func (client *SyncClient) RunLoop() { 47 | seq := 0 48 | msg := ReceiveMessage(client.conn) 49 | if msg == nil { 50 | return 51 | } 52 | if msg.Cmd != MSG_STORAGE_SYNC_BEGIN { 53 | return 54 | } 55 | 56 | cursor := msg.Body.(*SyncCursor) 57 | log.Info("cursor msgid:", cursor.msgid) 58 | c := client.storage.LoadSyncMessagesInBackground(cursor.msgid) 59 | 60 | for batch := range c { 61 | msg := &Message{Cmd: MSG_STORAGE_SYNC_MESSAGE_BATCH, Body: batch} 62 | seq = seq + 1 63 | msg.Seq = seq 64 | SendMessage(client.conn, msg) 65 | } 66 | 67 | client.master.AddClient(client) 68 | defer client.master.RemoveClient(client) 69 | 70 | for { 71 | msg := <-client.ewt 72 | if msg == nil { 73 | log.Warning("chan closed") 74 | break 75 | } 76 | 77 | seq = seq + 1 78 | msg.Seq = seq 79 | err := SendMessage(client.conn, msg) 80 | if err != nil { 81 | break 82 | } 83 | } 84 | } 85 | 86 | func (client *SyncClient) Run() { 87 | go client.RunLoop() 88 | } 89 | -------------------------------------------------------------------------------- /tests/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import struct 3 | import socket 4 | import threading 5 | import time 6 | import requests 7 | import json 8 | import uuid 9 | import base64 10 | import hashlib 11 | import sys 12 | import ssl 13 | import random 14 | import redis 15 | import config 16 | 17 | 18 | from protocol import * 19 | 20 | 21 | KEFU_APP_ID = config.KEFU_APP_ID 22 | APP_ID = config.APP_ID 23 | HOST = config.HOST 24 | SSL = config.SSL 25 | 26 | 27 | rds = redis.StrictRedis( 28 | host=config.REDIS_HOST, 29 | password=config.REDIS_PASSWORD, 30 | port=config.REDIS_PORT, 31 | db=config.REDIS_DB, 32 | decode_responses=True, 33 | ) 34 | 35 | 36 | UNICODE_ASCII_CHARACTER_SET = ( 37 | "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" 38 | ) 39 | 40 | 41 | def random_token_generator(length=30, chars=UNICODE_ASCII_CHARACTER_SET): 42 | rand = random.SystemRandom() 43 | return "".join(rand.choice(chars) for x in range(length)) 44 | 45 | 46 | def create_access_token(): 47 | return random_token_generator() 48 | 49 | 50 | class User(object): 51 | @staticmethod 52 | def get_user_access_token(rds, appid, uid): 53 | key = "users_%d_%d" % (appid, uid) 54 | token = rds.hget(key, "access_token") 55 | return token 56 | 57 | @staticmethod 58 | def load_user_access_token(rds, token): 59 | key = "access_token_%s" % token 60 | exists = rds.exists(key) 61 | if not exists: 62 | return 0, 0, "" 63 | uid, appid, name = rds.hget(key, "user_id", "app_id", "user_name") 64 | return uid, appid, name 65 | 66 | @staticmethod 67 | def save_user(rds, appid, uid, name, avatar, token): 68 | key = "users_%d_%d" % (appid, uid) 69 | obj = {"access_token": token, "name": name, "avatar": avatar} 70 | rds.hmset(key, obj) 71 | 72 | @staticmethod 73 | def save_token(rds, appid, uid, token): 74 | key = "access_token_%s" % token 75 | obj = {"user_id": uid, "app_id": appid} 76 | rds.hmset(key, obj) 77 | 78 | @staticmethod 79 | def save_user_access_token(rds, appid, uid, name, token): 80 | pipe = rds.pipeline() 81 | 82 | key = "access_token_%s" % token 83 | obj = {"user_id": uid, "user_name": name, "app_id": appid} 84 | 85 | pipe.hmset(key, obj) 86 | 87 | key = "users_%d_%d" % (appid, uid) 88 | obj = {"access_token": token, "name": name} 89 | 90 | pipe.hmset(key, obj) 91 | pipe.execute() 92 | 93 | return True 94 | 95 | 96 | def _login(appid, uid): 97 | token = User.get_user_access_token(rds, appid, uid) 98 | if not token: 99 | token = create_access_token() 100 | User.save_user_access_token(rds, appid, uid, "", token) 101 | return token 102 | 103 | 104 | def _connect_server(token, port): 105 | seq = 0 106 | if SSL: 107 | address = (HOST, 24430) 108 | sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 109 | sock = ssl.wrap_socket(sock_fd) 110 | else: 111 | address = (HOST, port) 112 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 113 | 114 | print("connect address:", address) 115 | sock.connect(address) 116 | auth = AuthenticationToken() 117 | auth.token = token 118 | seq = seq + 1 119 | send_message(MSG_AUTH_TOKEN, seq, auth, sock) 120 | cmd, _, _, msg = recv_message(sock) 121 | if cmd != MSG_AUTH_STATUS or msg != 0: 122 | raise Exception("auth failure:" + token) 123 | return sock, seq 124 | 125 | 126 | def recv_client_(uid, sock, seq, handler, group_id=None): 127 | group_sync_keys = {} 128 | sync_key = 0 129 | 130 | seq += 1 131 | send_message(MSG_SYNC, seq, sync_key, sock) 132 | if group_id: 133 | group_sync_keys[group_id] = 0 134 | seq += 1 135 | send_message(MSG_SYNC_GROUP, seq, (group_id, sync_key), sock) 136 | quit = False 137 | begin = False 138 | while True: 139 | cmd, s, flag, msg = recv_message(sock) 140 | print("cmd:", cmd, "msg:", msg) 141 | if cmd == MSG_SYNC_BEGIN: 142 | begin = True 143 | elif cmd == MSG_SYNC_END: 144 | begin = False 145 | new_sync_key = msg 146 | if new_sync_key > sync_key: 147 | sync_key = new_sync_key 148 | seq += 1 149 | send_message(MSG_SYNC_KEY, seq, sync_key, sock) 150 | if quit: 151 | break 152 | elif cmd == MSG_SYNC_NOTIFY: 153 | new_sync_key = msg 154 | if new_sync_key > sync_key: 155 | seq += 1 156 | send_message(MSG_SYNC, seq, sync_key, sock) 157 | elif cmd == MSG_SYNC_GROUP_NOTIFY: 158 | group_id, new_sync_key = msg 159 | skey = group_sync_keys.get(group_id, 0) 160 | if new_sync_key > skey: 161 | seq += 1 162 | send_message(MSG_SYNC_GROUP, seq, (group_id, skey), sock) 163 | elif cmd == MSG_SYNC_GROUP_BEGIN: 164 | begin = True 165 | elif cmd == MSG_SYNC_GROUP_END: 166 | begin = False 167 | group_id, new_sync_key = msg 168 | skey = group_sync_keys.get(group_id, 0) 169 | if new_sync_key > skey: 170 | group_sync_keys[group_id] = new_sync_key 171 | skey = group_sync_keys.get(group_id, 0) 172 | seq += 1 173 | send_message(MSG_GROUP_SYNC_KEY, seq, (group_id, skey), sock) 174 | if quit: 175 | break 176 | 177 | elif not (flag & MESSAGE_FLAG_PUSH) and handler(cmd, s, msg): 178 | quit = True 179 | if not begin: 180 | break 181 | 182 | sock.close() 183 | 184 | 185 | def connect_server(uid, port, appid=None): 186 | if appid is None: 187 | token = _login(APP_ID, uid) 188 | else: 189 | token = _login(appid, uid) 190 | if not token: 191 | raise Exception("login failure") 192 | return _connect_server(token, port) 193 | 194 | 195 | def kefu_connect_server(uid, port): 196 | return connect_server(uid, port, KEFU_APP_ID) 197 | 198 | 199 | def kefu_recv_client(uid, port, handler): 200 | sock, seq = kefu_connect_server(uid, port) 201 | recv_client_(uid, sock, seq, handler) 202 | 203 | 204 | def recv_group_client(uid, group_id, port, handler): 205 | sock, seq = connect_server(uid, port) 206 | recv_client_(uid, sock, seq, handler, group_id=group_id) 207 | 208 | 209 | def recv_client(uid, port, handler, appid=None): 210 | sock, seq = connect_server(uid, port, appid) 211 | recv_client_(uid, sock, seq, handler) 212 | -------------------------------------------------------------------------------- /tests/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | REDIS_HOST = "127.0.0.1" 4 | REDIS_PORT = 6379 5 | REDIS_DB = 0 6 | REDIS_PASSWORD = "" 7 | 8 | 9 | KEFU_APP_ID = 1453 10 | APP_ID = 7 11 | 12 | HOST = "127.0.0.1" 13 | SSL = False 14 | 15 | RPC_URL = "http://localhost:6666" 16 | -------------------------------------------------------------------------------- /tests/rpc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import config 4 | import logging 5 | import json 6 | import requests 7 | 8 | im_url = "http://127.0.0.1:6666" 9 | 10 | 11 | 12 | def post_peer_message(appid, sender, receiver, content): 13 | params = { 14 | "appid":appid, 15 | "receiver":receiver, 16 | "sender":sender 17 | } 18 | 19 | url = im_url + "/post_peer_message" 20 | res = requests.post(url, data=content.encode("utf-8"), params=params) 21 | return res 22 | 23 | 24 | def post_group_message(appid, sender, receiver, content): 25 | params = { 26 | "appid":appid, 27 | "sender":sender, 28 | "receiver":receiver, 29 | } 30 | url = im_url + "/post_group_message" 31 | res = requests.post(url, data=content.encode("utf-8"), params=params) 32 | return res 33 | 34 | 35 | def post_group_notification_s(appid, gid, notification, members): 36 | url = im_url + "/post_group_notification" 37 | 38 | obj = { 39 | "appid": appid, 40 | "group_id": gid, 41 | "notification":notification 42 | } 43 | if members: 44 | obj["members"] = members 45 | 46 | headers = {"Content-Type":"application/json"} 47 | 48 | data = json.dumps(obj) 49 | resp = requests.post(url, data=data, headers=headers) 50 | if resp.status_code != 200: 51 | logging.warning("send group notification error:%s", resp.content) 52 | else: 53 | logging.debug("send group notification success:%s", data) 54 | return resp 55 | 56 | def post_group_notification(appid, gid, op, members): 57 | try: 58 | return post_group_notification_s(appid, gid, json.dumps(op), members) 59 | except Exception as e: 60 | logging.warning("send group notification err:%s", e) 61 | return None 62 | 63 | 64 | 65 | def send_group_notification(appid, gid, op, members): 66 | return post_group_notification(appid, gid, op, members) 67 | 68 | 69 | 70 | def post_peer_notification(appid, uid, content): 71 | params = { 72 | "appid":appid, 73 | "uid":uid 74 | } 75 | url = im_url + "/post_notification" 76 | 77 | headers = {"Content-Type":"text/plain; charset=UTF-8"} 78 | resp = requests.post(url, data=content.encode("utf8"), headers=headers, params=params) 79 | return resp 80 | 81 | 82 | 83 | def post_system_message(appid, uid, content): 84 | url = im_url + "/post_system_message" 85 | 86 | headers = {"Content-Type":"text/plain; charset=UTF-8"} 87 | data = { 88 | "content": content, 89 | "receivers":[uid], 90 | "appid":appid, 91 | } 92 | print("data:", json.dumps(data)) 93 | resp = requests.post(url, data=json.dumps(data), headers=headers) 94 | return resp 95 | 96 | 97 | def post_room_message(appid, uid, room_id, content): 98 | params = { 99 | "appid":appid, 100 | "uid":uid, 101 | "room":room_id 102 | } 103 | url = im_url + "/post_room_message" 104 | headers = {"Content-Type":"text/plain; charset=UTF-8"} 105 | resp = requests.post(url, data=content.encode("utf8"), headers=headers, params=params) 106 | return resp 107 | 108 | -------------------------------------------------------------------------------- /tests/test_customer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import struct 4 | import socket 5 | import threading 6 | import time 7 | import requests 8 | import json 9 | import uuid 10 | import base64 11 | import sys 12 | from protocol import * 13 | from client import * 14 | 15 | 16 | task = 0 17 | 18 | 19 | def recv_customer_message_client_v2(appid, uid, port=23000): 20 | global task 21 | def handle_message(cmd, s, msg): 22 | if cmd == MSG_CUSTOMER_V2: 23 | return True 24 | else: 25 | return False 26 | 27 | recv_client(uid, port, handle_message, appid) 28 | task += 1 29 | print("recv customer message v2 success") 30 | 31 | 32 | def send_customer_message_v2(sender_appid, uid, receiver_appid, receiver): 33 | global task 34 | sock, seq = connect_server(uid, 23000, sender_appid) 35 | 36 | m = CustomerMessageV2() 37 | m.sender_appid = sender_appid 38 | m.sender = uid 39 | m.receiver_appid = receiver_appid 40 | m.receiver = receiver 41 | m.timestamp = 0 42 | m.content = json.dumps({"text":"test"}) 43 | seq += 1 44 | send_message(MSG_CUSTOMER_V2, seq, m, sock) 45 | print("send customer message v2 success") 46 | task += 1 47 | 48 | 49 | def recv_customer_message_client(uid, port=23000): 50 | global task 51 | def handle_message(cmd, s, msg): 52 | if cmd == MSG_CUSTOMER: 53 | return True 54 | else: 55 | return False 56 | 57 | kefu_recv_client(uid, port, handle_message) 58 | task += 1 59 | print("recv customer message success") 60 | 61 | 62 | def send_customer_message(uid, seller_id): 63 | global task 64 | sock, seq = connect_server(uid, 23000) 65 | 66 | m = CustomerMessage() 67 | m.customer_appid = APP_ID 68 | m.customer_id = uid 69 | m.store_id = 1 70 | m.seller_id = seller_id 71 | m.content = json.dumps({"text":"test"}) 72 | m.persistent = True 73 | seq += 1 74 | send_message(MSG_CUSTOMER, seq, m, sock) 75 | print("send customer message success") 76 | task += 1 77 | 78 | 79 | def recv_customer_support_message_client(uid, port=23000): 80 | global task 81 | def handle_message(cmd, s, msg): 82 | if cmd == MSG_CUSTOMER_SUPPORT: 83 | print("mmm:", msg) 84 | return True 85 | else: 86 | return False 87 | 88 | recv_client(uid, port, handle_message) 89 | task += 1 90 | print("recv customer support message success") 91 | 92 | 93 | def send_customer_support_message(seller_id, customer_id): 94 | global task 95 | sock, seq = kefu_connect_server(seller_id, 23000) 96 | 97 | m = CustomerMessage() 98 | m.customer_appid = APP_ID 99 | m.customer_id = customer_id 100 | m.store_id = 1 101 | m.seller_id = seller_id 102 | m.content = json.dumps({"text":"test"}) 103 | m.persistent = True 104 | seq += 1 105 | send_message(MSG_CUSTOMER_SUPPORT, seq, m, sock) 106 | print("send customer support message success") 107 | task += 1 108 | 109 | 110 | def TestCustomerSupportMessage(): 111 | global task 112 | task = 0 113 | 114 | t3 = threading.Thread(target=recv_customer_support_message_client, args=(1, )) 115 | t3.setDaemon(True) 116 | t3.start() 117 | 118 | time.sleep(1) 119 | 120 | t2 = threading.Thread(target=send_customer_support_message, args=(2, 1)) 121 | t2.setDaemon(True) 122 | t2.start() 123 | 124 | while task < 2: 125 | time.sleep(1) 126 | 127 | print("test customer support message completed") 128 | 129 | 130 | def TestCustomerMessage(): 131 | global task 132 | task = 0 133 | 134 | t3 = threading.Thread(target=recv_customer_message_client, args=(2, )) 135 | t3.setDaemon(True) 136 | t3.start() 137 | 138 | time.sleep(1) 139 | 140 | t2 = threading.Thread(target=send_customer_message, args=(1, 2)) 141 | t2.setDaemon(True) 142 | t2.start() 143 | 144 | while task < 2: 145 | time.sleep(1) 146 | 147 | print("test customer message completed") 148 | 149 | def TestCustomerMessageV2(): 150 | global task 151 | task = 0 152 | 153 | t3 = threading.Thread(target=recv_customer_message_client_v2, args=(KEFU_APP_ID, 2)) 154 | t3.setDaemon(True) 155 | t3.start() 156 | 157 | time.sleep(1) 158 | 159 | t2 = threading.Thread(target=send_customer_message_v2, args=(APP_ID, 1, KEFU_APP_ID, 2)) 160 | t2.setDaemon(True) 161 | t2.start() 162 | 163 | while task < 2: 164 | time.sleep(1) 165 | 166 | print("test customer message v2 completed") 167 | 168 | 169 | def main(): 170 | TestCustomerMessage() 171 | TestCustomerSupportMessage() 172 | TestCustomerMessageV2() 173 | 174 | if __name__ == "__main__": 175 | main() 176 | 177 | -------------------------------------------------------------------------------- /tests/test_room.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import struct 4 | import socket 5 | import threading 6 | import time 7 | import requests 8 | import json 9 | import uuid 10 | import base64 11 | import sys 12 | from protocol import * 13 | from client import * 14 | 15 | task = 0 16 | 17 | 18 | def recv_room_client(uid, port, room_id, handler): 19 | sock, seq = connect_server(uid, port) 20 | 21 | seq += 1 22 | send_message(MSG_ENTER_ROOM, seq, room_id, sock) 23 | 24 | while True: 25 | cmd, s, _, msg = recv_message(sock) 26 | seq += 1 27 | send_message(MSG_ACK, seq, s, sock) 28 | if handler(cmd, s, msg): 29 | break 30 | 31 | 32 | def recv_room_message_client(uid, room_id, port=23000): 33 | global task 34 | def handle_message(cmd, s, msg): 35 | if cmd == MSG_ROOM_IM: 36 | return True 37 | else: 38 | return False 39 | 40 | recv_room_client(uid, port, room_id, handle_message) 41 | task += 1 42 | print("recv room message success") 43 | 44 | 45 | def send_room_message_client(uid, room_id): 46 | global task 47 | sock, seq = connect_server(uid, 23000) 48 | 49 | seq += 1 50 | send_message(MSG_ENTER_ROOM, seq, room_id, sock) 51 | 52 | im = RTMessage() 53 | im.sender = uid 54 | im.receiver = room_id 55 | im.content = "test room im" 56 | 57 | for _ in range(1000): 58 | seq += 1 59 | send_message(MSG_ROOM_IM, seq, im, sock) 60 | 61 | task += 1 62 | print("send success") 63 | 64 | 65 | 66 | 67 | def _TestRoomMessage(port): 68 | global task 69 | task = 0 70 | 71 | room_id = 1 72 | t3 = threading.Thread(target=recv_room_message_client, args=(13635273143, room_id, port)) 73 | t3.setDaemon(True) 74 | t3.start() 75 | 76 | time.sleep(1) 77 | 78 | 79 | t2 = threading.Thread(target=send_room_message_client, args=(13635273142, room_id)) 80 | t2.setDaemon(True) 81 | t2.start() 82 | 83 | while task < 1: 84 | time.sleep(1) 85 | 86 | def TestRoomMessage(): 87 | _TestRoomMessage(23000) 88 | print("test room message completed") 89 | 90 | def TestClusterRoomMessage(): 91 | _TestRoomMessage(24000) 92 | print("test cluster room message completed") 93 | 94 | 95 | 96 | def main(): 97 | cluster = False 98 | 99 | 100 | print("test room message") 101 | TestRoomMessage() 102 | time.sleep(1) 103 | 104 | if cluster: 105 | TestClusterRoomMessage() 106 | 107 | 108 | 109 | 110 | if __name__ == "__main__": 111 | main() 112 | 113 | -------------------------------------------------------------------------------- /tools/truncate.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-2015, GoBelieve 3 | * All rights reserved. 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "bytes" 24 | "encoding/binary" 25 | "flag" 26 | "fmt" 27 | "os" 28 | "path/filepath" 29 | "strconv" 30 | "strings" 31 | 32 | log "github.com/sirupsen/logrus" 33 | ) 34 | 35 | const HEADER_SIZE = 32 36 | const MAGIC = 0x494d494d 37 | const VERSION = 1 << 16 //1.0 38 | 39 | const BLOCK_SIZE = 128 * 1024 * 1024 40 | 41 | var root string 42 | 43 | func init() { 44 | flag.StringVar(&root, "root", "", "root") 45 | } 46 | 47 | func checkRoot(root string) { 48 | pattern := fmt.Sprintf("%s/message_*", root) 49 | files, _ := filepath.Glob(pattern) 50 | block_NO := 0 //begin from 0 51 | for _, f := range files { 52 | base := filepath.Base(f) 53 | if strings.HasPrefix(base, "message_") { 54 | if !checkFile(f) { 55 | log.Warning("check file failure") 56 | r := truncateFile(f) 57 | log.Info("truncate file:", r) 58 | } else { 59 | log.Infof("check file pass:%s", f) 60 | } 61 | b, err := strconv.ParseInt(base[8:], 10, 64) 62 | if err != nil { 63 | log.Fatal("invalid message file:", f) 64 | } 65 | 66 | if int(b) > block_NO { 67 | block_NO = int(b) 68 | } 69 | } 70 | } 71 | 72 | } 73 | 74 | // 校验文件结尾是否合法 75 | func checkFile(file_path string) bool { 76 | file, err := os.Open(file_path) 77 | if err != nil { 78 | log.Fatal("open file:", err) 79 | } 80 | 81 | file_size, err := file.Seek(0, os.SEEK_END) 82 | if err != nil { 83 | log.Fatal("seek file") 84 | } 85 | 86 | if file_size == HEADER_SIZE { 87 | return true 88 | } 89 | 90 | if file_size < HEADER_SIZE { 91 | return false 92 | } 93 | 94 | _, err = file.Seek(file_size-4, os.SEEK_SET) 95 | if err != nil { 96 | log.Fatal("seek file") 97 | } 98 | 99 | mf := make([]byte, 4) 100 | n, err := file.Read(mf) 101 | if err != nil || n != 4 { 102 | log.Fatal("read file err:", err) 103 | } 104 | buffer := bytes.NewBuffer(mf) 105 | var m int32 106 | binary.Read(buffer, binary.BigEndian, &m) 107 | 108 | passed := int(m) == MAGIC 109 | if !passed { 110 | log.Infof("file tail magic:%x %d", m, m) 111 | } 112 | 113 | return passed 114 | } 115 | 116 | func truncateFile(file_path string) bool { 117 | file, err := os.Open(file_path) 118 | if err != nil { 119 | log.Fatal("open file:", err) 120 | } 121 | 122 | file_size, err := file.Seek(0, os.SEEK_END) 123 | if err != nil { 124 | log.Fatal("seek file") 125 | } 126 | 127 | if file_size == HEADER_SIZE { 128 | return true 129 | } 130 | 131 | if file_size < HEADER_SIZE { 132 | return false 133 | } 134 | 135 | offset := int64(4) 136 | 137 | for { 138 | _, err = file.Seek(file_size-offset, os.SEEK_SET) 139 | if err != nil { 140 | log.Fatal("seek file") 141 | } 142 | 143 | mf := make([]byte, 4) 144 | n, err := file.Read(mf) 145 | if err != nil || n != 4 { 146 | log.Fatal("read file err:", err) 147 | } 148 | buffer := bytes.NewBuffer(mf) 149 | var m int32 150 | binary.Read(buffer, binary.BigEndian, &m) 151 | 152 | if int(m) == MAGIC { 153 | log.Infof("file name:%s size:%d truncated:%d passed", file_path, file_size, file_size-offset+4) 154 | return true 155 | } 156 | 157 | offset += 4 158 | } 159 | 160 | return false 161 | 162 | } 163 | 164 | // 判断所给路径是否为文件夹 165 | func IsDir(path string) bool { 166 | s, err := os.Stat(path) 167 | if err != nil { 168 | return false 169 | } 170 | return s.IsDir() 171 | } 172 | 173 | func main() { 174 | flag.Parse() 175 | 176 | if len(root) == 0 { 177 | log.Info("trunncate imsroot") 178 | return 179 | } 180 | 181 | if !IsDir(root) { 182 | log.Info(root, "is not dir") 183 | return 184 | } 185 | log.Info("checking root:", root) 186 | checkRoot(root) 187 | } 188 | --------------------------------------------------------------------------------