├── docs
├── arch.png
├── benchmark.jpg
├── goim.graffle
├── handshake.png
├── protocol.png
├── benchmark-comet.jpg
├── benchmark-flow.jpg
├── benchmark-heap.jpg
├── benchmark_cn.md
├── benchmark_en.md
├── proto.md
├── en
│ ├── push.md
│ └── proto.md
└── push.md
├── pkg
├── time
│ ├── debug.go
│ ├── duration_test.go
│ ├── duration.go
│ ├── timer_test.go
│ └── timer.go
├── ip
│ ├── ip_test.go
│ └── ip.go
├── bytes
│ ├── buffer_test.go
│ ├── writer_test.go
│ ├── writer.go
│ └── buffer.go
├── strings
│ ├── ints_test.go
│ └── ints.go
├── encoding
│ └── binary
│ │ ├── endian_test.go
│ │ └── endian.go
└── websocket
│ ├── server_test.go
│ ├── server.go
│ ├── request.go
│ └── conn.go
├── cmd
├── job
│ ├── job-example.toml
│ └── main.go
├── comet
│ ├── comet-example.toml
│ └── main.go
└── logic
│ ├── logic-example.toml
│ └── main.go
├── scripts
├── README.md
├── jdk8.sh
├── zk.sh
└── kafka.sh
├── examples
├── javascript
│ ├── main.go
│ ├── index.html
│ └── client.js
├── cert.pem
└── private.pem
├── api
├── protocol
│ ├── protocol.proto
│ ├── operation.go
│ └── protocol.pb.go
├── generate.go
├── comet
│ └── comet.proto
└── logic
│ └── logic.proto
├── .gitignore
├── internal
├── logic
│ ├── model
│ │ ├── online.go
│ │ ├── metadata.go
│ │ └── room.go
│ ├── logic_test.go
│ ├── http
│ │ ├── nodes.go
│ │ ├── result.go
│ │ ├── server.go
│ │ ├── online.go
│ │ ├── middleware.go
│ │ └── push.go
│ ├── nodes_test.go
│ ├── dao
│ │ ├── dao_test.go
│ │ ├── kafka_test.go
│ │ ├── redis_test.go
│ │ ├── dao.go
│ │ └── kafka.go
│ ├── online_test.go
│ ├── push_test.go
│ ├── conn_test.go
│ ├── online.go
│ ├── push.go
│ ├── nodes.go
│ ├── balancer_test.go
│ ├── conn.go
│ ├── grpc
│ │ └── server.go
│ ├── logic.go
│ ├── conf
│ │ └── conf.go
│ └── balancer.go
├── comet
│ ├── whitelist.go
│ ├── errors
│ │ └── errors.go
│ ├── ring.go
│ ├── room.go
│ ├── round.go
│ ├── channel.go
│ ├── operation.go
│ ├── grpc
│ │ └── server.go
│ ├── server.go
│ ├── conf
│ │ └── conf.go
│ └── bucket.go
└── job
│ ├── conf
│ └── conf.go
│ ├── push.go
│ ├── room.go
│ ├── job.go
│ └── comet.go
├── codecov.sh
├── CHANGELOG.md
├── .github
└── workflows
│ └── go.yml
├── Makefile
├── LICENSE
├── go.mod
├── benchmarks
├── push_room
│ └── main.go
├── push_rooms
│ └── main.go
├── push
│ └── main.go
├── multi_push
│ └── main.go
└── client
│ └── main.go
├── README_en.md
├── README.md
└── README_cn.md
/docs/arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/arch.png
--------------------------------------------------------------------------------
/docs/benchmark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/benchmark.jpg
--------------------------------------------------------------------------------
/docs/goim.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/goim.graffle
--------------------------------------------------------------------------------
/docs/handshake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/handshake.png
--------------------------------------------------------------------------------
/docs/protocol.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/protocol.png
--------------------------------------------------------------------------------
/docs/benchmark-comet.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/benchmark-comet.jpg
--------------------------------------------------------------------------------
/docs/benchmark-flow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/benchmark-flow.jpg
--------------------------------------------------------------------------------
/docs/benchmark-heap.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terry-Mao/goim/HEAD/docs/benchmark-heap.jpg
--------------------------------------------------------------------------------
/pkg/time/debug.go:
--------------------------------------------------------------------------------
1 | package time
2 |
3 | const (
4 | // Debug debug switch
5 | Debug = false
6 | )
7 |
--------------------------------------------------------------------------------
/pkg/ip/ip_test.go:
--------------------------------------------------------------------------------
1 | package ip
2 |
3 | import "testing"
4 |
5 | func TestIP(t *testing.T) {
6 | ip := InternalIP()
7 | if ip == "" {
8 | t.FailNow()
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/cmd/job/job-example.toml:
--------------------------------------------------------------------------------
1 | # This is a TOML document. Boom
2 | [discovery]
3 | nodes = ["127.0.0.1:7171"]
4 |
5 | [kafka]
6 | topic = "goim-push-topic"
7 | group = "goim-push-group-job"
8 | brokers = ["127.0.0.1:9092"]
9 |
--------------------------------------------------------------------------------
/scripts/README.md:
--------------------------------------------------------------------------------
1 | # Installing
2 |
3 | ### Install JDK
4 | ```
5 | ./jdk.sh
6 | ```
7 |
8 | ### Install Zookeeper
9 | ```
10 | ./zk.sh
11 | ```
12 |
13 | ### Install Kafka
14 | ```
15 | ./kafka.sh
16 | ```
17 |
--------------------------------------------------------------------------------
/examples/javascript/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | )
7 |
8 | func main() {
9 | // Simple static webserver:
10 | log.Fatal(http.ListenAndServe(":1999", http.FileServer(http.Dir("./"))))
11 | }
12 |
--------------------------------------------------------------------------------
/api/protocol/protocol.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package goim.protocol;
4 |
5 | option go_package = "github.com/Terry-Mao/goim/api/protocol;protocol";
6 |
7 | /*
8 | * v1.0.0
9 | * protocol
10 | */
11 | message Proto {
12 | int32 ver = 1;
13 | int32 op = 2;
14 | int32 seq = 3;
15 | bytes body = 4;
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDE ignore
2 | .idea/
3 | *.ipr
4 | *.iml
5 | *.iws
6 | .vscode/
7 |
8 | # temp ignore
9 | *.log
10 | *.cache
11 | *.diff
12 | *.exe
13 | *.exe~
14 | *.patch
15 | *.tmp
16 | *.swp
17 |
18 | # system ignore
19 | .DS_Store
20 | Thumbs.db
21 |
22 | # build
23 | /cmd/comet/comet
24 | /cmd/logic/logic
25 | /cmd/job/job
26 | /target
27 | /configs
28 | /dist
29 |
--------------------------------------------------------------------------------
/api/generate.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | //go:generate protoc -I. -I$GOPATH/src --go_out=plugins=grpc:. --go_opt=paths=source_relative protocol/protocol.proto
4 | //go:generate protoc -I. -I$GOPATH/src --go_out=plugins=grpc:. --go_opt=paths=source_relative comet/comet.proto
5 | //go:generate protoc -I. -I$GOPATH/src --go_out=plugins=grpc:. --go_opt=paths=source_relative logic/logic.proto
6 |
--------------------------------------------------------------------------------
/internal/logic/model/online.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // Online ip and room online.
4 | type Online struct {
5 | Server string `json:"server"`
6 | RoomCount map[string]int32 `json:"room_count"`
7 | Updated int64 `json:"updated"`
8 | }
9 |
10 | // Top top sorted.
11 | type Top struct {
12 | RoomID string `json:"room_id"`
13 | Count int32 `json:"count"`
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/time/duration_test.go:
--------------------------------------------------------------------------------
1 | package time
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestDurationText(t *testing.T) {
9 | var (
10 | input = []byte("10s")
11 | output = time.Second * 10
12 | d Duration
13 | )
14 | if err := d.UnmarshalText(input); err != nil {
15 | t.FailNow()
16 | }
17 | if int64(output) != int64(d) {
18 | t.FailNow()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/codecov.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | echo "" > coverage.txt
5 |
6 | for d in $(go list ./... | grep -v cmd | grep -v docs | grep -v srcipts | grep -v benchmarks | grep -v examples); do
7 | echo "testing for $d ..."
8 | go test -coverprofile=profile.out -covermode=atomic $d
9 | if [ -f profile.out ]; then
10 | cat profile.out >> coverage.txt
11 | rm profile.out
12 | fi
13 | done
14 |
--------------------------------------------------------------------------------
/pkg/time/duration.go:
--------------------------------------------------------------------------------
1 | package time
2 |
3 | import (
4 | xtime "time"
5 | )
6 |
7 | // Duration be used toml unmarshal string time, like 1s, 500ms.
8 | type Duration xtime.Duration
9 |
10 | // UnmarshalText unmarshal text to duration.
11 | func (d *Duration) UnmarshalText(text []byte) error {
12 | tmp, err := xtime.ParseDuration(string(text))
13 | if err == nil {
14 | *d = Duration(tmp)
15 | }
16 | return err
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/bytes/buffer_test.go:
--------------------------------------------------------------------------------
1 | package bytes
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestBuffer(t *testing.T) {
8 | p := NewPool(2, 10)
9 | b := p.Get()
10 | if b.Bytes() == nil || len(b.Bytes()) == 0 {
11 | t.FailNow()
12 | }
13 | b = p.Get()
14 | if b.Bytes() == nil || len(b.Bytes()) == 0 {
15 | t.FailNow()
16 | }
17 | b = p.Get()
18 | if b.Bytes() == nil || len(b.Bytes()) == 0 {
19 | t.FailNow()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/internal/logic/model/metadata.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | const (
4 | // MetaWeight meta weight
5 | MetaWeight = "weight"
6 | // MetaOffline meta offline
7 | MetaOffline = "offline"
8 | // MetaAddrs meta public ip addrs
9 | MetaAddrs = "addrs"
10 | // MetaIPCount meta ip count
11 | MetaIPCount = "ip_count"
12 | // MetaConnCount meta conn count
13 | MetaConnCount = "conn_count"
14 |
15 | // PlatformWeb platform web
16 | PlatformWeb = "web"
17 | )
18 |
--------------------------------------------------------------------------------
/internal/logic/model/room.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | )
7 |
8 | // EncodeRoomKey encode a room key.
9 | func EncodeRoomKey(typ string, room string) string {
10 | return fmt.Sprintf("%s://%s", typ, room)
11 | }
12 |
13 | // DecodeRoomKey decode room key.
14 | func DecodeRoomKey(key string) (string, string, error) {
15 | u, err := url.Parse(key)
16 | if err != nil {
17 | return "", "", err
18 | }
19 | return u.Scheme, u.Host, nil
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/strings/ints_test.go:
--------------------------------------------------------------------------------
1 | package strings
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestInt32(t *testing.T) {
9 | i := []int32{1, 2, 3}
10 | s := JoinInt32s(i, ",")
11 | ii, _ := SplitInt32s(s, ",")
12 | if !reflect.DeepEqual(i, ii) {
13 | t.FailNow()
14 | }
15 | }
16 |
17 | func TestInt64(t *testing.T) {
18 | i := []int64{1, 2, 3}
19 | s := JoinInt64s(i, ",")
20 | ii, _ := SplitInt64s(s, ",")
21 | if !reflect.DeepEqual(i, ii) {
22 | t.FailNow()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/bytes/writer_test.go:
--------------------------------------------------------------------------------
1 | package bytes
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestWriter(t *testing.T) {
9 | w := NewWriterSize(64)
10 | if w.Len() != 0 && w.Size() != 64 {
11 | t.FailNow()
12 | }
13 | b := []byte("hello")
14 | w.Write(b)
15 | if !reflect.DeepEqual(b, w.Buffer()) {
16 | t.FailNow()
17 | }
18 | w.Peek(len(b))
19 | w.Reset()
20 | for i := 0; i < 1024; i++ {
21 | w.Write(b)
22 | }
23 | w.Reset()
24 | if w.Len() != 0 {
25 | t.FailNow()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/internal/logic/logic_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "os"
7 | "testing"
8 |
9 | "github.com/Terry-Mao/goim/internal/logic/conf"
10 | )
11 |
12 | var (
13 | lg *Logic
14 | )
15 |
16 | func TestMain(m *testing.M) {
17 | if err := flag.Set("conf", "../../cmd/logic/logic-example.toml"); err != nil {
18 | panic(err)
19 | }
20 | flag.Parse()
21 | if err := conf.Init(); err != nil {
22 | panic(err)
23 | }
24 | lg = New(conf.Conf)
25 | if err := lg.Ping(context.TODO()); err != nil {
26 | panic(err)
27 | }
28 | os.Exit(m.Run())
29 | }
30 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | #### goim
2 |
3 | ##### Version 2.0.0
4 | > 1.router has been changed to redis
5 | > 2.Support node with redis online heartbeat maintenance
6 | > 3.Support for gRPC and Discovery services
7 | > 4.Support node connection number and weight scheduling
8 | > 5.Support node scheduling by region
9 | > 6.Support instruction subscription
10 | > 7.Support the current connection room switch
11 | > 8.Support multiple room types ({type}://{room_id})
12 | > 9.Support sending messages by device_id
13 | > 10.Support for room message aggregation
14 | > 11.Supports IPv6
15 |
--------------------------------------------------------------------------------
/internal/logic/http/nodes.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func (s *Server) nodesWeighted(c *gin.Context) {
10 | var arg struct {
11 | Platform string `form:"platform"`
12 | }
13 | if err := c.BindQuery(&arg); err != nil {
14 | errors(c, RequestErr, err.Error())
15 | return
16 | }
17 | res := s.logic.NodesWeighted(c, arg.Platform, c.ClientIP())
18 | result(c, res, OK)
19 | }
20 |
21 | func (s *Server) nodesInstances(c *gin.Context) {
22 | res := s.logic.NodesInstances(context.TODO())
23 | result(c, res, OK)
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/encoding/binary/endian_test.go:
--------------------------------------------------------------------------------
1 | package binary
2 |
3 | import "testing"
4 |
5 | func TestInt8(t *testing.T) {
6 | b := make([]byte, 1)
7 | BigEndian.PutInt8(b, 100)
8 | i := BigEndian.Int8(b)
9 | if i != 100 {
10 | t.FailNow()
11 | }
12 | }
13 |
14 | func TestInt16(t *testing.T) {
15 | b := make([]byte, 2)
16 | BigEndian.PutInt16(b, 100)
17 | i := BigEndian.Int16(b)
18 | if i != 100 {
19 | t.FailNow()
20 | }
21 | }
22 |
23 | func TestInt32(t *testing.T) {
24 | b := make([]byte, 4)
25 | BigEndian.PutInt32(b, 100)
26 | i := BigEndian.Int32(b)
27 | if i != 100 {
28 | t.FailNow()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/scripts/jdk8.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # config parameters
3 | JDK_HOME=/usr/local/jdk8/
4 | # download jdk
5 | curl -L https://download.oracle.com/otn-pub/java/jdk/8u191-b12/2787e4a523244c269598db4e85c51e0c/jdk-8u191-linux-x64.tar.gz -o jdk-8u191-linux-x64.tar.gz
6 | tar zxf jdk-8u191-linux-x64.tar.gz
7 | # install jdk
8 | mkdir -p $JDK_HOME
9 | mv jdk1.8.0/* $JDK_HOME
10 | update-alternatives --install "/usr/bin/java" "java" "$JDK_HOME/bin/java" 1500
11 | update-alternatives --install "/usr/bin/javac" "javac" "$JDK_HOME/bin/javac" 1500
12 | update-alternatives --install "/usr/bin/javaws" "javaws" "$JDK_HOME/bin/javaws" 1500
13 |
--------------------------------------------------------------------------------
/internal/logic/nodes_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/bilibili/discovery/naming"
8 | "github.com/Terry-Mao/goim/internal/logic/model"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestNodes(t *testing.T) {
14 | var (
15 | c = context.TODO()
16 | clientIP = "127.0.0.1"
17 | )
18 | lg.nodes = make([]*naming.Instance, 0)
19 | ins := lg.NodesInstances(c)
20 | assert.NotNil(t, ins)
21 | nodes := lg.NodesWeighted(c, model.PlatformWeb, clientIP)
22 | assert.NotNil(t, nodes)
23 | nodes = lg.NodesWeighted(c, "android", clientIP)
24 | assert.NotNil(t, nodes)
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/ip/ip.go:
--------------------------------------------------------------------------------
1 | package ip
2 |
3 | import (
4 | "net"
5 | "strings"
6 | )
7 |
8 | // InternalIP return internal ip.
9 | func InternalIP() string {
10 | inters, err := net.Interfaces()
11 | if err != nil {
12 | return ""
13 | }
14 | for _, inter := range inters {
15 | if inter.Flags&net.FlagUp != 0 && !strings.HasPrefix(inter.Name, "lo") {
16 | addrs, err := inter.Addrs()
17 | if err != nil {
18 | continue
19 | }
20 | for _, addr := range addrs {
21 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
22 | if ipnet.IP.To4() != nil {
23 | return ipnet.IP.String()
24 | }
25 | }
26 | }
27 | }
28 | }
29 | return ""
30 | }
31 |
--------------------------------------------------------------------------------
/internal/logic/dao/dao_test.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "os"
7 | "testing"
8 |
9 | "github.com/Terry-Mao/goim/internal/logic/conf"
10 | )
11 |
12 | var (
13 | d *Dao
14 | )
15 |
16 | func TestMain(m *testing.M) {
17 | if err := flag.Set("conf", "../../../cmd/logic/logic-example.toml"); err != nil {
18 | panic(err)
19 | }
20 | flag.Parse()
21 | if err := conf.Init(); err != nil {
22 | panic(err)
23 | }
24 | d = New(conf.Conf)
25 | if err := d.Ping(context.TODO()); err != nil {
26 | os.Exit(-1)
27 | }
28 | if err := d.Close(); err != nil {
29 | os.Exit(-1)
30 | }
31 | if err := d.Ping(context.TODO()); err == nil {
32 | os.Exit(-1)
33 | }
34 | d = New(conf.Conf)
35 | os.Exit(m.Run())
36 | }
37 |
--------------------------------------------------------------------------------
/docs/benchmark_cn.md:
--------------------------------------------------------------------------------
1 | ## 压测图表
2 | 
3 |
4 | ### 服务端配置
5 | | CPU | 内存 | 操作系统 | 数量 |
6 | | :---- | :---- | :---- | :---- |
7 | | Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz | DDR3 32GB | Debian GNU/Linux 8 | 1 |
8 |
9 | ### 压测参数
10 | * 不同UID同房间在线人数: 1,000,000
11 | * 持续推送时长: 15分钟
12 | * 持续推送数量: 40条/秒
13 | * 推送内容: {"test":1}
14 | * 推送类型: 单房间推送
15 | * 到达计算方式: 1秒统计一次,共30次
16 |
17 | ### 资源使用
18 | * 每台服务端CPU使用: 2000%~2300%(刚好满负载)
19 | * 每台服务端内存使用: 14GB左右
20 | * GC耗时: 504毫秒左右
21 | * 流量使用: Incoming(450MBit/s), Outgoing(4.39GBit/s)
22 |
23 | ### 压测结果
24 | * 推送到达: 3590万/秒左右;
25 |
26 | ## comet模块
27 | 
28 |
29 | ## 流量
30 | 
31 |
32 | ## heap信息(包含GC)
33 | 
34 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | build:
12 | name: Build
13 | runs-on: ubuntu-latest
14 | steps:
15 |
16 | - name: Set up Go 1.x
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: ^1.13
20 |
21 | - name: Check out code into the Go module directory
22 | uses: actions/checkout@v2
23 |
24 | - name: Get dependencies
25 | run: |
26 | go get -v -t -d ./...
27 | if [ -f Gopkg.toml ]; then
28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
29 | dep ensure
30 | fi
31 |
32 | - name: Build
33 | run: go build -v ./...
34 |
--------------------------------------------------------------------------------
/pkg/encoding/binary/endian.go:
--------------------------------------------------------------------------------
1 | package binary
2 |
3 | // BigEndian big endian.
4 | var BigEndian bigEndian
5 |
6 | type bigEndian struct{}
7 |
8 | func (bigEndian) Int8(b []byte) int8 { return int8(b[0]) }
9 |
10 | func (bigEndian) PutInt8(b []byte, v int8) {
11 | b[0] = byte(v)
12 | }
13 |
14 | func (bigEndian) Int16(b []byte) int16 { return int16(b[1]) | int16(b[0])<<8 }
15 |
16 | func (bigEndian) PutInt16(b []byte, v int16) {
17 | _ = b[1]
18 | b[0] = byte(v >> 8)
19 | b[1] = byte(v)
20 | }
21 |
22 | func (bigEndian) Int32(b []byte) int32 {
23 | return int32(b[3]) | int32(b[2])<<8 | int32(b[1])<<16 | int32(b[0])<<24
24 | }
25 |
26 | func (bigEndian) PutInt32(b []byte, v int32) {
27 | _ = b[3]
28 | b[0] = byte(v >> 24)
29 | b[1] = byte(v >> 16)
30 | b[2] = byte(v >> 8)
31 | b[3] = byte(v)
32 | }
33 |
--------------------------------------------------------------------------------
/internal/logic/http/result.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | const (
8 | // OK ok
9 | OK = 0
10 | // RequestErr request error
11 | RequestErr = -400
12 | // ServerErr server error
13 | ServerErr = -500
14 |
15 | contextErrCode = "context/err/code"
16 | )
17 |
18 | type resp struct {
19 | Code int `json:"code"`
20 | Message string `json:"message"`
21 | Data interface{} `json:"data,omitempty"`
22 | }
23 |
24 | func errors(c *gin.Context, code int, msg string) {
25 | c.Set(contextErrCode, code)
26 | c.JSON(200, resp{
27 | Code: code,
28 | Message: msg,
29 | })
30 | }
31 |
32 | func result(c *gin.Context, data interface{}, code int) {
33 | c.Set(contextErrCode, code)
34 | c.JSON(200, resp{
35 | Code: code,
36 | Data: data,
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/docs/benchmark_en.md:
--------------------------------------------------------------------------------
1 | ## Benchmark Chart
2 | 
3 |
4 | ### Benchmark Server
5 | | CPU | Memory | OS | Instance |
6 | | :---- | :---- | :---- | :---- |
7 | | Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz | DDR3 32GB | Debian GNU/Linux 8 | 1 |
8 |
9 | ### Benchmark Case
10 | * Online: 1,000,000
11 | * Duration: 15min
12 | * Push Speed: 40/s (broadcast room)
13 | * Push Message: {"test":1}
14 | * Received calc mode: 1s per times, total 30 times
15 |
16 | ### Benchmark Resource
17 |
18 | * CPU: 2000%~2300%
19 | * Memory: 14GB
20 | * GC Pause: 504ms
21 | * Network: Incoming(450MBit/s), Outgoing(4.39GBit/s)
22 |
23 | ### Benchmark Result
24 | * Received: 35,900,000/s
25 |
26 | ## Comet
27 | 
28 |
29 | ## Network traffic
30 | 
31 |
32 | ## Heap (include GC)
33 | 
34 |
--------------------------------------------------------------------------------
/internal/logic/dao/kafka_test.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestDaoPushMsg(t *testing.T) {
11 | var (
12 | c = context.Background()
13 | op = int32(100)
14 | server = "test"
15 | msg = []byte("msg")
16 | keys = []string{"key"}
17 | )
18 | err := d.PushMsg(c, op, server, keys, msg)
19 | assert.Nil(t, err)
20 | }
21 |
22 | func TestDaoBroadcastRoomMsg(t *testing.T) {
23 | var (
24 | c = context.Background()
25 | op = int32(100)
26 | room = "test://1"
27 | msg = []byte("msg")
28 | )
29 | err := d.BroadcastRoomMsg(c, op, room, msg)
30 | assert.Nil(t, err)
31 | }
32 |
33 | func TestDaoBroadcastMsg(t *testing.T) {
34 | var (
35 | c = context.Background()
36 | op = int32(100)
37 | speed = int32(0)
38 | msg = []byte("")
39 | )
40 | err := d.BroadcastMsg(c, op, speed, msg)
41 | assert.Nil(t, err)
42 | }
43 |
--------------------------------------------------------------------------------
/internal/logic/online_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestOnline(t *testing.T) {
11 | var (
12 | c = context.TODO()
13 | typ = "test"
14 | n = 2
15 | rooms = []string{"room_01", "room_02", "room_03"}
16 | )
17 | lg.totalIPs = 100
18 | lg.totalConns = 200
19 | lg.roomCount = map[string]int32{
20 | "test://room_01": 100,
21 | "test://room_02": 200,
22 | "test://room_03": 300,
23 | }
24 | tops, err := lg.OnlineTop(c, typ, n)
25 | assert.Nil(t, err)
26 | assert.Equal(t, len(tops), 2)
27 | onlines, err := lg.OnlineRoom(c, typ, rooms)
28 | assert.Nil(t, err)
29 | assert.Equal(t, onlines["room_01"], int32(100))
30 | assert.Equal(t, onlines["room_02"], int32(200))
31 | assert.Equal(t, onlines["room_03"], int32(300))
32 | ips, conns := lg.OnlineTotal(c)
33 | assert.Equal(t, ips, int64(100))
34 | assert.Equal(t, conns, int64(200))
35 | }
36 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Go parameters
2 | GOCMD=GO111MODULE=on go
3 | GOBUILD=$(GOCMD) build
4 | GOTEST=$(GOCMD) test
5 |
6 | all: test build
7 | build:
8 | rm -rf target/
9 | mkdir target/
10 | cp cmd/comet/comet-example.toml target/comet.toml
11 | cp cmd/logic/logic-example.toml target/logic.toml
12 | cp cmd/job/job-example.toml target/job.toml
13 | $(GOBUILD) -o target/comet cmd/comet/main.go
14 | $(GOBUILD) -o target/logic cmd/logic/main.go
15 | $(GOBUILD) -o target/job cmd/job/main.go
16 |
17 | test:
18 | $(GOTEST) -v ./...
19 |
20 | clean:
21 | rm -rf target/
22 |
23 | run:
24 | nohup target/logic -conf=target/logic.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 2>&1 > target/logic.log &
25 | nohup target/comet -conf=target/comet.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 -addrs=127.0.0.1 -debug=true 2>&1 > target/comet.log &
26 | nohup target/job -conf=target/job.toml -region=sh -zone=sh001 -deploy.env=dev 2>&1 > target/job.log &
27 |
28 | stop:
29 | pkill -f target/logic
30 | pkill -f target/job
31 | pkill -f target/comet
32 |
--------------------------------------------------------------------------------
/cmd/comet/comet-example.toml:
--------------------------------------------------------------------------------
1 | # This is a TOML document. Boom
2 | [discovery]
3 | nodes = ["127.0.0.1:7171"]
4 |
5 | [rpcServer]
6 | addr = ":3109"
7 | timeout = "1s"
8 |
9 | [rpcClient]
10 | dial = "1s"
11 | timeout = "1s"
12 |
13 | [tcp]
14 | bind = [":3101"]
15 | sndbuf = 4096
16 | rcvbuf = 4096
17 | keepalive = false
18 | reader = 32
19 | readBuf = 1024
20 | readBufSize = 8192
21 | writer = 32
22 | writeBuf = 1024
23 | writeBufSize = 8192
24 |
25 | [websocket]
26 | bind = [":3102"]
27 | tlsOpen = false
28 | tlsBind = [":3103"]
29 | certFile = "../../cert.pem"
30 | privateFile = "../../private.pem"
31 |
32 | [protocol]
33 | timer = 32
34 | timerSize = 2048
35 | svrProto = 10
36 | cliProto = 5
37 | handshakeTimeout = "8s"
38 |
39 | [whitelist]
40 | Whitelist = [123]
41 | WhiteLog = "/tmp/white_list.log"
42 |
43 | [bucket]
44 | size = 32
45 | channel = 1024
46 | room = 1024
47 | routineAmount = 32
48 | routineSize = 1024
49 |
--------------------------------------------------------------------------------
/pkg/bytes/writer.go:
--------------------------------------------------------------------------------
1 | package bytes
2 |
3 | // Writer writer.
4 | type Writer struct {
5 | n int
6 | buf []byte
7 | }
8 |
9 | // NewWriterSize new a writer with size.
10 | func NewWriterSize(n int) *Writer {
11 | return &Writer{buf: make([]byte, n)}
12 | }
13 |
14 | // Len buff len.
15 | func (w *Writer) Len() int {
16 | return w.n
17 | }
18 |
19 | // Size buff cap.
20 | func (w *Writer) Size() int {
21 | return len(w.buf)
22 | }
23 |
24 | // Reset reset the buff.
25 | func (w *Writer) Reset() {
26 | w.n = 0
27 | }
28 |
29 | // Buffer return buff.
30 | func (w *Writer) Buffer() []byte {
31 | return w.buf[:w.n]
32 | }
33 |
34 | // Peek peek a buf.
35 | func (w *Writer) Peek(n int) []byte {
36 | var buf []byte
37 | w.grow(n)
38 | buf = w.buf[w.n : w.n+n]
39 | w.n += n
40 | return buf
41 | }
42 |
43 | // Write write a buff.
44 | func (w *Writer) Write(p []byte) {
45 | w.grow(len(p))
46 | w.n += copy(w.buf[w.n:], p)
47 | }
48 |
49 | func (w *Writer) grow(n int) {
50 | var buf []byte
51 | if w.n+n < len(w.buf) {
52 | return
53 | }
54 | buf = make([]byte, 2*len(w.buf)+n)
55 | copy(buf, w.buf[:w.n])
56 | w.buf = buf
57 | }
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Terry.Mao
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/internal/comet/whitelist.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/Terry-Mao/goim/internal/comet/conf"
8 | )
9 |
10 | var whitelist *Whitelist
11 |
12 | // Whitelist .
13 | type Whitelist struct {
14 | log *log.Logger
15 | list map[int64]struct{} // whitelist for debug
16 | }
17 |
18 | // InitWhitelist a whitelist struct.
19 | func InitWhitelist(c *conf.Whitelist) (err error) {
20 | var (
21 | mid int64
22 | f *os.File
23 | )
24 | if f, err = os.OpenFile(c.WhiteLog, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644); err == nil {
25 | whitelist = new(Whitelist)
26 | whitelist.log = log.New(f, "", log.LstdFlags)
27 | whitelist.list = make(map[int64]struct{})
28 | for _, mid = range c.Whitelist {
29 | whitelist.list[mid] = struct{}{}
30 | }
31 | }
32 | return
33 | }
34 |
35 | // Contains whitelist contains a mid or not.
36 | func (w *Whitelist) Contains(mid int64) (ok bool) {
37 | if mid > 0 {
38 | _, ok = w.list[mid]
39 | }
40 | return
41 | }
42 |
43 | // Printf calls l.Output to print to the logger.
44 | func (w *Whitelist) Printf(format string, v ...interface{}) {
45 | w.log.Printf(format, v...)
46 | }
47 |
--------------------------------------------------------------------------------
/internal/logic/push_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestPushKeys(t *testing.T) {
11 | var (
12 | c = context.TODO()
13 | op = int32(100)
14 | keys = []string{"test_key"}
15 | msg = []byte("hello")
16 | )
17 | err := lg.PushKeys(c, op, keys, msg)
18 | assert.Nil(t, err)
19 | }
20 |
21 | func TestPushMids(t *testing.T) {
22 | var (
23 | c = context.TODO()
24 | op = int32(100)
25 | mids = []int64{1, 2, 3}
26 | msg = []byte("hello")
27 | )
28 | err := lg.PushMids(c, op, mids, msg)
29 | assert.Nil(t, err)
30 | }
31 |
32 | func TestPushRoom(t *testing.T) {
33 | var (
34 | c = context.TODO()
35 | op = int32(100)
36 | typ = "test"
37 | room = "test_room"
38 | msg = []byte("hello")
39 | )
40 | err := lg.PushRoom(c, op, typ, room, msg)
41 | assert.Nil(t, err)
42 | }
43 |
44 | func TestPushAll(t *testing.T) {
45 | var (
46 | c = context.TODO()
47 | op = int32(100)
48 | speed = int32(100)
49 | msg = []byte("hello")
50 | )
51 | err := lg.PushAll(c, op, speed, msg)
52 | assert.Nil(t, err)
53 | }
54 |
--------------------------------------------------------------------------------
/examples/javascript/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | client demo
7 |
8 |
16 |
19 |
20 |
21 | websocket
22 | status:
23 |
24 | push
25 |
26 |
curl -d 'mid message' 'http://api.goim.io:3111/goim/push/mids?operation=1000&mids=123'
27 |
curl -d 'room message' 'http://api.goim.io:3111/goim/push/room?operation=1000&type=live&room=1000'
28 |
curl -d 'broadcast message' 'http://api.goim.io:3111/goim/push/all?operation=1000'
29 |
30 | message:
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/internal/comet/errors/errors.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | // .
8 | var (
9 | // server
10 | ErrHandshake = errors.New("handshake failed")
11 | ErrOperation = errors.New("request operation not valid")
12 | // ring
13 | ErrRingEmpty = errors.New("ring buffer empty")
14 | ErrRingFull = errors.New("ring buffer full")
15 | // timer
16 | ErrTimerFull = errors.New("timer full")
17 | ErrTimerEmpty = errors.New("timer empty")
18 | ErrTimerNoItem = errors.New("timer item not exist")
19 | // channel
20 | ErrPushMsgArg = errors.New("rpc pushmsg arg error")
21 | ErrPushMsgsArg = errors.New("rpc pushmsgs arg error")
22 | ErrMPushMsgArg = errors.New("rpc mpushmsg arg error")
23 | ErrMPushMsgsArg = errors.New("rpc mpushmsgs arg error")
24 | ErrSignalFullMsgDropped = errors.New("signal channel full, msg dropped")
25 | // bucket
26 | ErrBroadCastArg = errors.New("rpc broadcast arg error")
27 | ErrBroadCastRoomArg = errors.New("rpc broadcast room arg error")
28 |
29 | // room
30 | ErrRoomDroped = errors.New("room droped")
31 | // rpc
32 | ErrLogic = errors.New("logic rpc is not available")
33 | )
34 |
--------------------------------------------------------------------------------
/pkg/time/timer_test.go:
--------------------------------------------------------------------------------
1 | package time
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | log "github.com/golang/glog"
8 | )
9 |
10 | func TestTimer(t *testing.T) {
11 | timer := NewTimer(100)
12 | tds := make([]*TimerData, 100)
13 | for i := 0; i < 100; i++ {
14 | tds[i] = timer.Add(time.Duration(i)*time.Second+5*time.Minute, nil)
15 | }
16 | printTimer(timer)
17 | for i := 0; i < 100; i++ {
18 | log.Infof("td: %s, %s, %d", tds[i].Key, tds[i].ExpireString(), tds[i].index)
19 | timer.Del(tds[i])
20 | }
21 | printTimer(timer)
22 | for i := 0; i < 100; i++ {
23 | tds[i] = timer.Add(time.Duration(i)*time.Second+5*time.Minute, nil)
24 | }
25 | printTimer(timer)
26 | for i := 0; i < 100; i++ {
27 | timer.Del(tds[i])
28 | }
29 | printTimer(timer)
30 | timer.Add(time.Second, nil)
31 | time.Sleep(time.Second * 2)
32 | if len(timer.timers) != 0 {
33 | t.FailNow()
34 | }
35 | }
36 |
37 | func printTimer(timer *Timer) {
38 | log.Infof("----------timers: %d ----------", len(timer.timers))
39 | for i := 0; i < len(timer.timers); i++ {
40 | log.Infof("timer: %s, %s, index: %d", timer.timers[i].Key, timer.timers[i].ExpireString(), timer.timers[i].index)
41 | }
42 | log.Infof("--------------------")
43 | }
44 |
--------------------------------------------------------------------------------
/cmd/job/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 |
9 | "github.com/bilibili/discovery/naming"
10 | "github.com/Terry-Mao/goim/internal/job"
11 | "github.com/Terry-Mao/goim/internal/job/conf"
12 |
13 | resolver "github.com/bilibili/discovery/naming/grpc"
14 | log "github.com/golang/glog"
15 | )
16 |
17 | var (
18 | ver = "2.0.0"
19 | )
20 |
21 | func main() {
22 | flag.Parse()
23 | if err := conf.Init(); err != nil {
24 | panic(err)
25 | }
26 | log.Infof("goim-job [version: %s env: %+v] start", ver, conf.Conf.Env)
27 | // grpc register naming
28 | dis := naming.New(conf.Conf.Discovery)
29 | resolver.Register(dis)
30 | // job
31 | j := job.New(conf.Conf)
32 | go j.Consume()
33 | // signal
34 | c := make(chan os.Signal, 1)
35 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
36 | for {
37 | s := <-c
38 | log.Infof("goim-job get a signal %s", s.String())
39 | switch s {
40 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
41 | j.Close()
42 | log.Infof("goim-job [version: %s] exit", ver)
43 | log.Flush()
44 | return
45 | case syscall.SIGHUP:
46 | default:
47 | return
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/api/comet/comet.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package goim.comet;
4 |
5 | option go_package = "github.com/Terry-Mao/goim/api/comet;comet";
6 |
7 | import "github.com/Terry-Mao/goim/api/protocol/protocol.proto";
8 |
9 | message PushMsgReq {
10 | repeated string keys = 1;
11 | int32 protoOp = 3;
12 | goim.protocol.Proto proto = 2;
13 | }
14 |
15 | message PushMsgReply {}
16 |
17 | message BroadcastReq{
18 | int32 protoOp = 1;
19 | goim.protocol.Proto proto = 2;
20 | int32 speed = 3;
21 | }
22 |
23 | message BroadcastReply{}
24 |
25 | message BroadcastRoomReq {
26 | string roomID = 1;
27 | goim.protocol.Proto proto = 2;
28 | }
29 |
30 | message BroadcastRoomReply{}
31 |
32 | message RoomsReq{}
33 |
34 | message RoomsReply {
35 | map rooms = 1;
36 | }
37 |
38 | service Comet {
39 | // PushMsg push by key or mid
40 | rpc PushMsg(PushMsgReq) returns (PushMsgReply);
41 | // Broadcast send to every enrity
42 | rpc Broadcast(BroadcastReq) returns (BroadcastReply);
43 | // BroadcastRoom broadcast to one room
44 | rpc BroadcastRoom(BroadcastRoomReq) returns (BroadcastRoomReply);
45 | // Rooms get all rooms
46 | rpc Rooms(RoomsReq) returns (RoomsReply);
47 | }
48 |
--------------------------------------------------------------------------------
/internal/logic/http/server.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "github.com/Terry-Mao/goim/internal/logic"
5 | "github.com/Terry-Mao/goim/internal/logic/conf"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | // Server is http server.
11 | type Server struct {
12 | engine *gin.Engine
13 | logic *logic.Logic
14 | }
15 |
16 | // New new a http server.
17 | func New(c *conf.HTTPServer, l *logic.Logic) *Server {
18 | engine := gin.New()
19 | engine.Use(loggerHandler, recoverHandler)
20 | go func() {
21 | if err := engine.Run(c.Addr); err != nil {
22 | panic(err)
23 | }
24 | }()
25 | s := &Server{
26 | engine: engine,
27 | logic: l,
28 | }
29 | s.initRouter()
30 | return s
31 | }
32 |
33 | func (s *Server) initRouter() {
34 | group := s.engine.Group("/goim")
35 | group.POST("/push/keys", s.pushKeys)
36 | group.POST("/push/mids", s.pushMids)
37 | group.POST("/push/room", s.pushRoom)
38 | group.POST("/push/all", s.pushAll)
39 | group.GET("/online/top", s.onlineTop)
40 | group.GET("/online/room", s.onlineRoom)
41 | group.GET("/online/total", s.onlineTotal)
42 | group.GET("/nodes/weighted", s.nodesWeighted)
43 | group.GET("/nodes/instances", s.nodesInstances)
44 | }
45 |
46 | // Close close the server.
47 | func (s *Server) Close() {
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/api/protocol/operation.go:
--------------------------------------------------------------------------------
1 | package protocol
2 |
3 | const (
4 | // OpHandshake handshake
5 | OpHandshake = int32(0)
6 | // OpHandshakeReply handshake reply
7 | OpHandshakeReply = int32(1)
8 |
9 | // OpHeartbeat heartbeat
10 | OpHeartbeat = int32(2)
11 | // OpHeartbeatReply heartbeat reply
12 | OpHeartbeatReply = int32(3)
13 |
14 | // OpSendMsg send message.
15 | OpSendMsg = int32(4)
16 | // OpSendMsgReply send message reply
17 | OpSendMsgReply = int32(5)
18 |
19 | // OpDisconnectReply disconnect reply
20 | OpDisconnectReply = int32(6)
21 |
22 | // OpAuth auth connnect
23 | OpAuth = int32(7)
24 | // OpAuthReply auth connect reply
25 | OpAuthReply = int32(8)
26 |
27 | // OpRaw raw message
28 | OpRaw = int32(9)
29 |
30 | // OpProtoReady proto ready
31 | OpProtoReady = int32(10)
32 | // OpProtoFinish proto finish
33 | OpProtoFinish = int32(11)
34 |
35 | // OpChangeRoom change room
36 | OpChangeRoom = int32(12)
37 | // OpChangeRoomReply change room reply
38 | OpChangeRoomReply = int32(13)
39 |
40 | // OpSub subscribe operation
41 | OpSub = int32(14)
42 | // OpSubReply subscribe operation
43 | OpSubReply = int32(15)
44 |
45 | // OpUnsub unsubscribe operation
46 | OpUnsub = int32(16)
47 | // OpUnsubReply unsubscribe operation reply
48 | OpUnsubReply = int32(17)
49 | )
50 |
--------------------------------------------------------------------------------
/internal/logic/http/online.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func (s *Server) onlineTop(c *gin.Context) {
10 | var arg struct {
11 | Type string `form:"type" binding:"required"`
12 | Limit int `form:"limit" binding:"required"`
13 | }
14 | if err := c.BindQuery(&arg); err != nil {
15 | errors(c, RequestErr, err.Error())
16 | return
17 | }
18 | res, err := s.logic.OnlineTop(c, arg.Type, arg.Limit)
19 | if err != nil {
20 | result(c, nil, RequestErr)
21 | return
22 | }
23 | result(c, res, OK)
24 | }
25 |
26 | func (s *Server) onlineRoom(c *gin.Context) {
27 | var arg struct {
28 | Type string `form:"type" binding:"required"`
29 | Rooms []string `form:"rooms" binding:"required"`
30 | }
31 | if err := c.BindQuery(&arg); err != nil {
32 | errors(c, RequestErr, err.Error())
33 | return
34 | }
35 | res, err := s.logic.OnlineRoom(c, arg.Type, arg.Rooms)
36 | if err != nil {
37 | result(c, nil, RequestErr)
38 | return
39 | }
40 | result(c, res, OK)
41 | }
42 |
43 | func (s *Server) onlineTotal(c *gin.Context) {
44 | ipCount, connCount := s.logic.OnlineTotal(context.TODO())
45 | res := map[string]interface{}{
46 | "ip_count": ipCount,
47 | "conn_count": connCount,
48 | }
49 | result(c, res, OK)
50 | }
51 |
--------------------------------------------------------------------------------
/internal/logic/conn_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/Terry-Mao/goim/api/protocol"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestConnect(t *testing.T) {
12 | var (
13 | server = "test_server"
14 | serverKey = "test_server_key"
15 | cookie = ""
16 | token = []byte(`{"mid":1, "key":"test_server_key", "room_id":"test://test_room", "platform":"web", "accepts":[1000,1001,1002]}`)
17 | ol = map[string]int32{"test://test_room": 100}
18 | c = context.Background()
19 | )
20 | // connect
21 | mid, key, roomID, accepts, hb, err := lg.Connect(c, server, cookie, token)
22 | assert.Nil(t, err)
23 | assert.Equal(t, serverKey, key)
24 | assert.Equal(t, roomID, "test://test_room")
25 | assert.Equal(t, len(accepts), 3)
26 | assert.NotZero(t, hb)
27 | t.Log(mid, key, roomID, accepts, err)
28 | // heartbeat
29 | err = lg.Heartbeat(c, mid, key, server)
30 | assert.Nil(t, err)
31 | // disconnect
32 | has, err := lg.Disconnect(c, mid, key, server)
33 | assert.Nil(t, err)
34 | assert.Equal(t, true, has)
35 | // renew
36 | online, err := lg.RenewOnline(c, server, ol)
37 | assert.Nil(t, err)
38 | assert.NotNil(t, online)
39 | // message
40 | err = lg.Receive(c, mid, &protocol.Proto{})
41 | assert.Nil(t, err)
42 | }
43 |
--------------------------------------------------------------------------------
/cmd/logic/logic-example.toml:
--------------------------------------------------------------------------------
1 | # This is a TOML document. Boom
2 | [discovery]
3 | nodes = ["127.0.0.1:7171"]
4 |
5 | [regions]
6 | "bj" = ["北京","天津","河北","山东","山西","内蒙古","辽宁","吉林","黑龙江","甘肃","宁夏","新疆"]
7 | "sh" = ["上海","江苏","浙江","安徽","江西","湖北","重庆","陕西","青海","河南","台湾"]
8 | "gz" = ["广东","福建","广西","海南","湖南","四川","贵州","云南","西藏","香港","澳门"]
9 |
10 | [node]
11 | defaultDomain = "conn.goim.io"
12 | hostDomain = ".goim.io"
13 | heartbeat = "4m"
14 | heartbeatMax = 2
15 | tcpPort = 3101
16 | wsPort = 3102
17 | wssPort = 3103
18 | regionWeight = 1.6
19 |
20 | [backoff]
21 | maxDelay = 300
22 | baseDelay = 3
23 | factor = 1.8
24 | jitter = 0.3
25 |
26 | [rpcServer]
27 | network = "tcp"
28 | addr = ":3119"
29 | timeout = "1s"
30 |
31 | [rpcClient]
32 | dial = "1s"
33 | timeout = "1s"
34 |
35 | [httpServer]
36 | network = "tcp"
37 | addr = ":3111"
38 | readTimeout = "1s"
39 | writeTimeout = "1s"
40 |
41 | [kafka]
42 | topic = "goim-push-topic"
43 | brokers = ["127.0.0.1:9092"]
44 |
45 | [redis]
46 | network = "tcp"
47 | addr = "127.0.0.1:6379"
48 | active = 60000
49 | idle = 1024
50 | dialTimeout = "200ms"
51 | readTimeout = "500ms"
52 | writeTimeout = "500ms"
53 | idleTimeout = "120s"
54 | expire = "30m"
55 |
--------------------------------------------------------------------------------
/internal/logic/http/middleware.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "fmt"
5 | "net/http/httputil"
6 | "runtime"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 | log "github.com/golang/glog"
11 | )
12 |
13 | func loggerHandler(c *gin.Context) {
14 | // Start timer
15 | start := time.Now()
16 | path := c.Request.URL.Path
17 | raw := c.Request.URL.RawQuery
18 | method := c.Request.Method
19 |
20 | // Process request
21 | c.Next()
22 |
23 | // Stop timer
24 | end := time.Now()
25 | latency := end.Sub(start)
26 | statusCode := c.Writer.Status()
27 | ecode := c.GetInt(contextErrCode)
28 | clientIP := c.ClientIP()
29 | if raw != "" {
30 | path = path + "?" + raw
31 | }
32 | log.Infof("METHOD:%s | PATH:%s | CODE:%d | IP:%s | TIME:%d | ECODE:%d", method, path, statusCode, clientIP, latency/time.Millisecond, ecode)
33 | }
34 |
35 | func recoverHandler(c *gin.Context) {
36 | defer func() {
37 | if err := recover(); err != nil {
38 | const size = 64 << 10
39 | buf := make([]byte, size)
40 | buf = buf[:runtime.Stack(buf, false)]
41 | httprequest, _ := httputil.DumpRequest(c.Request, false)
42 | pnc := fmt.Sprintf("[Recovery] %s panic recovered:\n%s\n%s\n%s", time.Now().Format("2006-01-02 15:04:05"), string(httprequest), err, buf)
43 | fmt.Print(pnc)
44 | log.Error(pnc)
45 | c.AbortWithStatus(500)
46 | }
47 | }()
48 | c.Next()
49 | }
50 |
--------------------------------------------------------------------------------
/internal/logic/online.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "sort"
6 | "strings"
7 |
8 | "github.com/Terry-Mao/goim/internal/logic/model"
9 | )
10 |
11 | var (
12 | _emptyTops = make([]*model.Top, 0)
13 | )
14 |
15 | // OnlineTop get the top online.
16 | func (l *Logic) OnlineTop(c context.Context, typ string, n int) (tops []*model.Top, err error) {
17 | for key, cnt := range l.roomCount {
18 | if strings.HasPrefix(key, typ) {
19 | _, roomID, err := model.DecodeRoomKey(key)
20 | if err != nil {
21 | continue
22 | }
23 | top := &model.Top{
24 | RoomID: roomID,
25 | Count: cnt,
26 | }
27 | tops = append(tops, top)
28 | }
29 | }
30 | sort.Slice(tops, func(i, j int) bool {
31 | return tops[i].Count > tops[j].Count
32 | })
33 | if len(tops) > n {
34 | tops = tops[:n]
35 | }
36 | if len(tops) == 0 {
37 | tops = _emptyTops
38 | }
39 | return
40 | }
41 |
42 | // OnlineRoom get rooms online.
43 | func (l *Logic) OnlineRoom(c context.Context, typ string, rooms []string) (res map[string]int32, err error) {
44 | res = make(map[string]int32, len(rooms))
45 | for _, room := range rooms {
46 | res[room] = l.roomCount[model.EncodeRoomKey(typ, room)]
47 | }
48 | return
49 | }
50 |
51 | // OnlineTotal get all online.
52 | func (l *Logic) OnlineTotal(c context.Context) (int64, int64) {
53 | return l.totalIPs, l.totalConns
54 | }
55 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Terry-Mao/goim
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/BurntSushi/toml v0.3.1
7 | github.com/Shopify/sarama v1.19.0 // indirect
8 | github.com/Shopify/toxiproxy v2.1.4+incompatible // indirect
9 | github.com/bilibili/discovery v1.0.1
10 | github.com/bsm/sarama-cluster v2.1.15+incompatible
11 | github.com/eapache/go-resiliency v1.1.0 // indirect
12 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect
13 | github.com/eapache/queue v1.1.0 // indirect
14 | github.com/gin-gonic/gin v1.7.0
15 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
16 | github.com/golang/protobuf v1.4.3
17 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
18 | github.com/gomodule/redigo v2.0.0+incompatible
19 | github.com/google/uuid v1.0.0
20 | github.com/onsi/ginkgo v1.16.0 // indirect
21 | github.com/onsi/gomega v1.11.0 // indirect
22 | github.com/pierrec/lz4 v2.0.5+incompatible // indirect
23 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect
24 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
25 | github.com/stretchr/testify v1.5.1
26 | github.com/zhenjl/cityhash v0.0.0-20131128155616-cdd6a94144ab
27 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb
28 | google.golang.org/grpc v1.22.3
29 | gopkg.in/Shopify/sarama.v1 v1.19.0
30 | )
31 |
--------------------------------------------------------------------------------
/examples/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIID3TCCAsWgAwIBAgIJAKWU8wETRh4fMA0GCSqGSIb3DQEBBQUAMIGEMQswCQYD
3 | VQQGEwJjbjERMA8GA1UECAwIc2hhbmdoYWkxETAPBgNVBAcMCHNoYW5naGFpMQ0w
4 | CwYDVQQKDARiaWxpMQ0wCwYDVQQLDARiaWxpMREwDwYDVQQDDAhiaWxpLmNvbTEe
5 | MBwGCSqGSIb3DQEJARYPMjI0MzAzNTRAcXEuY29tMB4XDTE1MDkwMTEyMDQxMloX
6 | DTI1MDgyOTEyMDQxMlowgYQxCzAJBgNVBAYTAmNuMREwDwYDVQQIDAhzaGFuZ2hh
7 | aTERMA8GA1UEBwwIc2hhbmdoYWkxDTALBgNVBAoMBGJpbGkxDTALBgNVBAsMBGJp
8 | bGkxETAPBgNVBAMMCGJpbGkuY29tMR4wHAYJKoZIhvcNAQkBFg8yMjQzMDM1NEBx
9 | cS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKFwSxJqYPxzMe
10 | m5PeYA4YmcUsDCqS9Z7PsszOMZ1YsWZIHMB74D49ad2R+9PoqlfNH1L9C4NFSBrF
11 | rhSkaLmFYxw9yeJ2EAPijASBgfxMFVrEJhu7SW86OPTVnHblU8UqQdnMFOqF49C9
12 | mCdbiGu/99BZVCL1WmlSQCWVEIzOgX+goxqHuwXUF58YUwr6WLtF0DuBcLUai1vB
13 | Pg+PJ2fLjSR2o0KJkPOd6+y90cgoxfyJFUHuUKyV8EU4VwHEIA9rVizprziqPx6c
14 | 9A9Ng0FpA2leSPLGYCjnDtKIOvbSOS8DMkRT55ujqoVrj0yiNWsuJlc/NbD6bS16
15 | fJjuLOtJAgMBAAGjUDBOMB0GA1UdDgQWBBQzxdSIYIkDABh98Cj6VeYasC7/STAf
16 | BgNVHSMEGDAWgBQzxdSIYIkDABh98Cj6VeYasC7/STAMBgNVHRMEBTADAQH/MA0G
17 | CSqGSIb3DQEBBQUAA4IBAQA1Fhr+SU62xHWlPOBhTbjod49+mNfXn2TZz/vBp/Jl
18 | pHZgDLAEcrhXHmi2A0G9K9+qOIEn4BvTd70jSYvYlaeUSzZ/nEpeM0oE0f2Qaxov
19 | PhxDpsqPsSQm6pE64/los1doaiElfMVFaP56UGV01kFdI013wxwd2WCuj51Hmvi9
20 | thsS027aqxjHMJnKXPvBm2E6EDkPfc/e+AEmwBzry+aamRizaMrk/SfSGTy9/rvd
21 | +VbBfHiJ50kMld51SLIc6qkVaTXess7mIfcsk7kyjP4eFA0y+3wmXfRZeWadND3I
22 | O9XNNwsDVFXlhW40GUVriy95qa1Sq3sLUfUQcCH4VFFK
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/benchmarks/push_room/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Start Commond eg: ./push_room 1 20 localhost:3111
4 | // first parameter: room id
5 | // second parameter: num per seconds
6 | // third parameter: logic server ip
7 |
8 | import (
9 | "bytes"
10 | "fmt"
11 | "io/ioutil"
12 | "net/http"
13 | "os"
14 | "strconv"
15 | "time"
16 | )
17 |
18 | func main() {
19 | rountineNum, err := strconv.Atoi(os.Args[2])
20 | if err != nil {
21 | panic(err)
22 | }
23 | addr := os.Args[3]
24 |
25 | gap := time.Second / time.Duration(rountineNum)
26 | delay := time.Duration(0)
27 |
28 | go run(addr, time.Duration(0)*time.Second)
29 | for i := 0; i < rountineNum-1; i++ {
30 | go run(addr, delay)
31 | delay += gap
32 | fmt.Println("delay:", delay)
33 | }
34 | time.Sleep(9999 * time.Hour)
35 | }
36 |
37 | func run(addr string, delay time.Duration) {
38 | time.Sleep(delay)
39 | i := int64(0)
40 | for {
41 | go post(addr, i)
42 | time.Sleep(time.Second)
43 | i++
44 | }
45 | }
46 |
47 | func post(addr string, i int64) {
48 | resp, err := http.Post("http://"+addr+"/goim/push/room?operation=1000&type=test&room="+os.Args[1], "application/json", bytes.NewBufferString(fmt.Sprintf("{\"test\":%d}", i)))
49 | if err != nil {
50 | fmt.Printf("Error: http.post() error(%v)\n", err)
51 | return
52 | }
53 | defer resp.Body.Close()
54 | body, err := ioutil.ReadAll(resp.Body)
55 | if err != nil {
56 | fmt.Printf("Error: http.post() error(%v)\n", err)
57 | return
58 | }
59 |
60 | fmt.Printf("%s postId:%d, response:%s\n", time.Now().Format("2006-01-02 15:04:05"), i, string(body))
61 | }
62 |
--------------------------------------------------------------------------------
/docs/proto.md:
--------------------------------------------------------------------------------
1 | # comet 客户端通讯协议文档
2 | comet支持两种协议和客户端通讯 websocket, tcp。
3 |
4 | ## websocket
5 | **请求URL**
6 |
7 | ws://DOMAIN/sub
8 |
9 | **HTTP请求方式**
10 |
11 | Websocket(JSON Frame),请求和返回协议一致
12 |
13 | **请求和返回json**
14 |
15 | ```json
16 | {
17 | "ver": 102,
18 | "op": 10,
19 | "seq": 10,
20 | "body": {"data": "xxx"}
21 | }
22 | ```
23 |
24 | **请求和返回参数说明**
25 |
26 | | 参数名 | 必选 | 类型 | 说明 |
27 | | :----- | :--- | :--- | :--- |
28 | | ver | true | int | 协议版本号 |
29 | | op | true | int | 指令 |
30 | | seq | true | int | 序列号(服务端返回和客户端发送一一对应) |
31 | | body | true | string | 授权令牌,用于检验获取用户真实用户Id |
32 |
33 | ## tcp
34 | **请求URL**
35 |
36 | tcp://DOMAIN
37 |
38 | **协议格式**
39 |
40 | 二进制,请求和返回协议一致
41 |
42 | **请求&返回参数**
43 |
44 | | 参数名 | 必选 | 类型 | 说明 |
45 | | :----- | :--- | :--- | :--- |
46 | | package length | true | int32 bigendian | 包长度 |
47 | | header Length | true | int16 bigendian | 包头长度 |
48 | | ver | true | int16 bigendian | 协议版本 |
49 | | operation | true | int32 bigendian | 协议指令 |
50 | | seq | true | int32 bigendian | 序列号 |
51 | | body | false | binary | $(package lenth) - $(header length) |
52 |
53 | ## 指令
54 | | 指令 | 说明 |
55 | | :----- | :--- |
56 | | 2 | 客户端请求心跳 |
57 | | 3 | 服务端心跳答复 |
58 | | 5 | 下行消息 |
59 | | 7 | auth认证 |
60 | | 8 | auth认证返回 |
61 |
62 |
--------------------------------------------------------------------------------
/pkg/websocket/server_test.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "net"
5 | "reflect"
6 | "testing"
7 | "time"
8 |
9 | "golang.org/x/net/websocket"
10 |
11 | "github.com/Terry-Mao/goim/pkg/bufio"
12 | )
13 |
14 | func TestServer(t *testing.T) {
15 | var (
16 | data = []byte{0, 1, 2}
17 | )
18 | ln, err := net.Listen("tcp", ":8080")
19 | if err != nil {
20 | t.FailNow()
21 | }
22 | go func() {
23 | conn, err := ln.Accept()
24 | if err != nil {
25 | t.Error(err)
26 | }
27 | rd := bufio.NewReader(conn)
28 | wr := bufio.NewWriter(conn)
29 | req, err := ReadRequest(rd)
30 | if err != nil {
31 | t.Error(err)
32 | }
33 | if req.RequestURI != "/sub" {
34 | t.Error(err)
35 | }
36 | ws, err := Upgrade(conn, rd, wr, req)
37 | if err != nil {
38 | t.Error(err)
39 | }
40 | if err = ws.WriteMessage(BinaryMessage, data); err != nil {
41 | t.Error(err)
42 | }
43 | if err = ws.Flush(); err != nil {
44 | t.Error(err)
45 | }
46 | op, b, err := ws.ReadMessage()
47 | if err != nil || op != BinaryMessage || !reflect.DeepEqual(b, data) {
48 | t.Error(err)
49 | }
50 | }()
51 | time.Sleep(time.Millisecond * 100)
52 | // ws client
53 | ws, err := websocket.Dial("ws://127.0.0.1:8080/sub", "", "*")
54 | if err != nil {
55 | t.FailNow()
56 | }
57 | // receive binary frame
58 | var b []byte
59 | if err = websocket.Message.Receive(ws, &b); err != nil {
60 | t.FailNow()
61 | }
62 | if !reflect.DeepEqual(b, data) {
63 | t.FailNow()
64 | }
65 | // send binary frame
66 | if err = websocket.Message.Send(ws, data); err != nil {
67 | t.FailNow()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/docs/en/push.md:
--------------------------------------------------------------------------------
1 | Terry-Mao/goim push HTTP protocols
2 | push HTTP interface protocols for pusher
3 |
4 | Interfaces
5 | | Name | URL | HTTP method |
6 | | :---- | :---- | :---- |
7 | | [single push](#single push) | /1/push | POST |
8 | | [multiple push](#multiple push) | /1/pushs | POST |
9 | | [room push](#room push) | /1/push/room | POST |
10 | | [broadcasting](#broadcasting) | /1/push/all | POST |
11 |
12 | Public response body
13 |
14 | | response code | description |
15 | | :---- | :---- |
16 | | 1 | success |
17 | | 65535 | internal error |
18 |
19 | Response structure
20 |
21 | {
22 | "ret": 1 //response code
23 | }
24 |
25 |
26 |
27 | ##### single push
28 | * Example request
29 |
30 | ```sh
31 | # uid is the user id pushing to?uid=0
32 | curl -d "{\"test\":1}" http://127.0.0.1:7172/1/push?uid=0
33 | ```
34 |
35 | * Response
36 |
37 |
38 | {
39 | "ret": 1
40 | }
41 |
42 |
43 | ##### Multiple push
44 | * Example request
45 |
46 | ```sh
47 | curl -d "{\"u\":[1,2,3,4,5],\"m\":{\"test\":1}}" http://127.0.0.1:7172/1/pushs
48 | ```
49 |
50 | * Response
51 |
52 |
53 | {
54 | "ret": 1
55 | }
56 |
57 |
58 | ##### room push
59 | * Example request
60 |
61 | ```sh
62 | curl -d "{\"test\": 1}" http://127.0.0.1:7172/1/push/room?rid=1
63 | ```
64 |
65 | * Response
66 |
67 |
68 | {
69 | "ret": 1
70 | }
71 |
72 |
73 | ##### Broadcasting
74 | * Example request
75 |
76 | ```sh
77 | curl -d "{\"test\": 1}" http://127.0.0.1:7172/1/push/all
78 | ```
79 |
80 | * Response
81 |
82 |
83 | {
84 | "ret": 1
85 | }
86 |
87 |
--------------------------------------------------------------------------------
/pkg/bytes/buffer.go:
--------------------------------------------------------------------------------
1 | package bytes
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | // Buffer buffer.
8 | type Buffer struct {
9 | buf []byte
10 | next *Buffer // next free buffer
11 | }
12 |
13 | // Bytes bytes.
14 | func (b *Buffer) Bytes() []byte {
15 | return b.buf
16 | }
17 |
18 | // Pool is a buffer pool.
19 | type Pool struct {
20 | lock sync.Mutex
21 | free *Buffer
22 | max int
23 | num int
24 | size int
25 | }
26 |
27 | // NewPool new a memory buffer pool struct.
28 | func NewPool(num, size int) (p *Pool) {
29 | p = new(Pool)
30 | p.init(num, size)
31 | return
32 | }
33 |
34 | // Init init the memory buffer.
35 | func (p *Pool) Init(num, size int) {
36 | p.init(num, size)
37 | }
38 |
39 | // init init the memory buffer.
40 | func (p *Pool) init(num, size int) {
41 | p.num = num
42 | p.size = size
43 | p.max = num * size
44 | p.grow()
45 | }
46 |
47 | // grow grow the memory buffer size, and update free pointer.
48 | func (p *Pool) grow() {
49 | var (
50 | i int
51 | b *Buffer
52 | bs []Buffer
53 | buf []byte
54 | )
55 | buf = make([]byte, p.max)
56 | bs = make([]Buffer, p.num)
57 | p.free = &bs[0]
58 | b = p.free
59 | for i = 1; i < p.num; i++ {
60 | b.buf = buf[(i-1)*p.size : i*p.size]
61 | b.next = &bs[i]
62 | b = b.next
63 | }
64 | b.buf = buf[(i-1)*p.size : i*p.size]
65 | b.next = nil
66 | }
67 |
68 | // Get get a free memory buffer.
69 | func (p *Pool) Get() (b *Buffer) {
70 | p.lock.Lock()
71 | if b = p.free; b == nil {
72 | p.grow()
73 | b = p.free
74 | }
75 | p.free = b.next
76 | p.lock.Unlock()
77 | return
78 | }
79 |
80 | // Put put back a memory buffer to free.
81 | func (p *Pool) Put(b *Buffer) {
82 | p.lock.Lock()
83 | b.next = p.free
84 | p.free = b
85 | p.lock.Unlock()
86 | }
87 |
--------------------------------------------------------------------------------
/examples/private.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpQIBAAKCAQEAyhcEsSamD8czHpuT3mAOGJnFLAwqkvWez7LMzjGdWLFmSBzA
3 | e+A+PWndkfvT6KpXzR9S/QuDRUgaxa4UpGi5hWMcPcnidhAD4owEgYH8TBVaxCYb
4 | u0lvOjj01Zx25VPFKkHZzBTqhePQvZgnW4hrv/fQWVQi9VppUkAllRCMzoF/oKMa
5 | h7sF1BefGFMK+li7RdA7gXC1GotbwT4Pjydny40kdqNCiZDznevsvdHIKMX8iRVB
6 | 7lCslfBFOFcBxCAPa1Ys6a84qj8enPQPTYNBaQNpXkjyxmAo5w7SiDr20jkvAzJE
7 | U+ebo6qFa49MojVrLiZXPzWw+m0tenyY7izrSQIDAQABAoIBAQCNasAwy2/nmKjw
8 | IUS/l44lru1oXncocdMZWvCw1c1a9IEzs1MLHKfRSBTyBDyNEy7v7pyfUQAiaku5
9 | y5DMYDB65BkuL+lWXuypCvxYOEL6ZvMmUdiUHdZE8vh5xsz4u788S+qCQpy+5uX6
10 | 1s+r4PIt2tekuxjfgs4y7YqfHn66PmxAWXTV+jnWpUWYjpRTgDWeU6ikbk2fHjvG
11 | ytF4AV4lEKNnGBC5F+5lk3QJKjb1IW/o5q42TCeERk1k5Ruc5kfzq72hdFYrQAF1
12 | PmstEXMExMS/K6AerYtPfiWWFWH2gCTZMf/C8Jwr0zPVpHM1PF+Ien3IkgiOTfZE
13 | 1+/wrx4BAoGBAOnpnxFnnqhZdDRi+DALfY3mXPwOizFYnAhuuuzO87zoZsps529E
14 | j5pEZQoYPTiww5rptjFhNjoV0gsh2GO5QHCiMxM8A76aWVc4YmK0GiHVudpAu2t+
15 | aK8+0Xr03SA27cKJjp12NWijzEdTjSQYaFwD/8/dMrU3MCu30kdEJ6iJAoGBAN0s
16 | JXLv4CtB/DLTvifTZ/OAGSc1/Z+X0w2EqIugw7IV9YuAm2721IFhsFFXeANRRHY6
17 | zonLUEgsuQUc30NCz+susChs2GMEFypLem8D1c5ZZY+9sIsR1WFB97o7bTMvp5lH
18 | 2REpEDZKXVOB7UmrclLgTKRQgG3O73rMP6QXYPzBAoGBAJm8Gvi0crlQuagol9fz
19 | 5Vwa2GgtItyW0U5VgHNdfSJeWBiYxO8DT6Jja0jcL3iP7K9nBYCk1KAOcVMxtmes
20 | fKbKY+kzW36tMSS7ASbAGiC8uH6yZru6hBERp1o5jw+6Kj/eaqYg5+9TIFKMnknn
21 | 5Mb9NecnCUnC8Nz63rBKIgqJAoGBAJHUpeyfFaPwIiYxT1RbJFN9xxf/lXdBWDu1
22 | mJxYKDCoIfsVlWcZAQ0+KE+56LvnPcjnBX/9urWcJ3KjkuJ6jzV211gQTK0c6VlN
23 | 4zCHytYAQ+L/JATOgW9bW8hDnsD9TvjWUt3pwXLKnbaOGLNWhE747g/5tHSy2VyS
24 | h/PeJmkBAoGABMfEaiLHXXQhUK0BPYxT3T8i9IjAYwXlrgSLlnvOGslZef/45kP5
25 | CM1UbMSwAn+HvAziOFt2WmynFCysy/lCyTud+Fd/IZFcMThp7wvi7fSZo0NDM7ES
26 | 9JfgTmCY4Kwv6kT85poIka9bp4Nh47EVB9kDoqm/lSMkfYqcWH66DJA=
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/internal/logic/push.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/Terry-Mao/goim/internal/logic/model"
7 |
8 | log "github.com/golang/glog"
9 | )
10 |
11 | // PushKeys push a message by keys.
12 | func (l *Logic) PushKeys(c context.Context, op int32, keys []string, msg []byte) (err error) {
13 | servers, err := l.dao.ServersByKeys(c, keys)
14 | if err != nil {
15 | return
16 | }
17 | pushKeys := make(map[string][]string)
18 | for i, key := range keys {
19 | server := servers[i]
20 | if server != "" && key != "" {
21 | pushKeys[server] = append(pushKeys[server], key)
22 | }
23 | }
24 | for server := range pushKeys {
25 | if err = l.dao.PushMsg(c, op, server, pushKeys[server], msg); err != nil {
26 | return
27 | }
28 | }
29 | return
30 | }
31 |
32 | // PushMids push a message by mid.
33 | func (l *Logic) PushMids(c context.Context, op int32, mids []int64, msg []byte) (err error) {
34 | keyServers, _, err := l.dao.KeysByMids(c, mids)
35 | if err != nil {
36 | return
37 | }
38 | keys := make(map[string][]string)
39 | for key, server := range keyServers {
40 | if key == "" || server == "" {
41 | log.Warningf("push key:%s server:%s is empty", key, server)
42 | continue
43 | }
44 | keys[server] = append(keys[server], key)
45 | }
46 | for server, keys := range keys {
47 | if err = l.dao.PushMsg(c, op, server, keys, msg); err != nil {
48 | return
49 | }
50 | }
51 | return
52 | }
53 |
54 | // PushRoom push a message by room.
55 | func (l *Logic) PushRoom(c context.Context, op int32, typ, room string, msg []byte) (err error) {
56 | return l.dao.BroadcastRoomMsg(c, op, model.EncodeRoomKey(typ, room), msg)
57 | }
58 |
59 | // PushAll push a message to all.
60 | func (l *Logic) PushAll(c context.Context, op, speed int32, msg []byte) (err error) {
61 | return l.dao.BroadcastMsg(c, op, speed, msg)
62 | }
63 |
--------------------------------------------------------------------------------
/internal/logic/dao/redis_test.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/Terry-Mao/goim/internal/logic/model"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestDaopingRedis(t *testing.T) {
12 | err := d.pingRedis(context.Background())
13 | assert.Nil(t, err)
14 | }
15 |
16 | func TestDaoAddMapping(t *testing.T) {
17 | var (
18 | c = context.Background()
19 | mid = int64(1)
20 | key = "test_key"
21 | server = "test_server"
22 | )
23 | err := d.AddMapping(c, 0, "test", server)
24 | assert.Nil(t, err)
25 | err = d.AddMapping(c, mid, key, server)
26 | assert.Nil(t, err)
27 |
28 | has, err := d.ExpireMapping(c, 0, "test")
29 | assert.Nil(t, err)
30 | assert.NotEqual(t, false, has)
31 | has, err = d.ExpireMapping(c, mid, key)
32 | assert.Nil(t, err)
33 | assert.NotEqual(t, false, has)
34 |
35 | res, err := d.ServersByKeys(c, []string{key})
36 | assert.Nil(t, err)
37 | assert.Equal(t, server, res[0])
38 |
39 | ress, mids, err := d.KeysByMids(c, []int64{mid})
40 | assert.Nil(t, err)
41 | assert.Equal(t, server, ress[key])
42 | assert.Equal(t, mid, mids[0])
43 |
44 | has, err = d.DelMapping(c, 0, "test", server)
45 | assert.Nil(t, err)
46 | assert.NotEqual(t, false, has)
47 | has, err = d.DelMapping(c, mid, key, server)
48 | assert.Nil(t, err)
49 | assert.NotEqual(t, false, has)
50 | }
51 |
52 | func TestDaoAddServerOnline(t *testing.T) {
53 | var (
54 | c = context.Background()
55 | server = "test_server"
56 | online = &model.Online{
57 | RoomCount: map[string]int32{"room": 10},
58 | }
59 | )
60 | err := d.AddServerOnline(c, server, online)
61 | assert.Nil(t, err)
62 |
63 | r, err := d.ServerOnline(c, server)
64 | assert.Nil(t, err)
65 | assert.Equal(t, online.RoomCount["room"], r.RoomCount["room"])
66 |
67 | err = d.DelServerOnline(c, server)
68 | assert.Nil(t, err)
69 | }
70 |
--------------------------------------------------------------------------------
/docs/en/proto.md:
--------------------------------------------------------------------------------
1 | # comet and clients protocols
2 | comet supports two protocols to communicate with client: WebSocket, TCP
3 |
4 | ## websocket
5 | **Request URL**
6 |
7 | ws://DOMAIN/sub
8 |
9 | **HTTP Request Method**
10 |
11 | WebSocket (JSON Frame). Response is same as the request.
12 |
13 | **Response Result**
14 |
15 | ```json
16 | {
17 | "ver": 102,
18 | "op": 10,
19 | "seq": 10,
20 | "body": {"data": "xxx"}
21 | }
22 | ```
23 |
24 | **Request and Response Parameters**
25 |
26 | | parameter | is required | type | comment|
27 | | :----- | :--- | :--- | :--- |
28 | | ver | true | int | Protocol version |
29 | | op | true | int | Operation |
30 | | seq | true | int | Sequence number (Server returned number maps to client sent) |
31 | | body | json | The JSON message pushed |
32 |
33 | ## tcp
34 | **Request URL**
35 |
36 | tcp://DOMAIN
37 |
38 | **Protocol**
39 |
40 | Binary. Response is same as the request.
41 |
42 | **Request and Response Parameters**
43 |
44 | | parameter | is required | type | comment|
45 | | :----- | :--- | :--- | :--- |
46 | | package length | true | int32 bigendian | package length |
47 | | header Length | true | int16 bigendian | header length |
48 | | ver | true | int16 bigendian | Protocol version |
49 | | operation | true | int32 bigendian | Operation |
50 | | seq | true | int32 bigendian | jsonp callback |
51 | | body | false | binary | $(package lenth) - $(header length) |
52 |
53 | ## Operations
54 | | operation | comment |
55 | | :----- | :--- |
56 | | 2 | Client send heartbeat|
57 | | 3 | Server reply heartbeat|
58 | | 7 | authentication request |
59 | | 8 | authentication response |
60 |
61 |
--------------------------------------------------------------------------------
/internal/logic/dao/dao.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/Terry-Mao/goim/internal/logic/conf"
8 | "github.com/gomodule/redigo/redis"
9 | kafka "gopkg.in/Shopify/sarama.v1"
10 | )
11 |
12 | // Dao dao.
13 | type Dao struct {
14 | c *conf.Config
15 | kafkaPub kafka.SyncProducer
16 | redis *redis.Pool
17 | redisExpire int32
18 | }
19 |
20 | // New new a dao and return.
21 | func New(c *conf.Config) *Dao {
22 | d := &Dao{
23 | c: c,
24 | kafkaPub: newKafkaPub(c.Kafka),
25 | redis: newRedis(c.Redis),
26 | redisExpire: int32(time.Duration(c.Redis.Expire) / time.Second),
27 | }
28 | return d
29 | }
30 |
31 | func newKafkaPub(c *conf.Kafka) kafka.SyncProducer {
32 | kc := kafka.NewConfig()
33 | kc.Producer.RequiredAcks = kafka.WaitForAll // Wait for all in-sync replicas to ack the message
34 | kc.Producer.Retry.Max = 10 // Retry up to 10 times to produce the message
35 | kc.Producer.Return.Successes = true
36 | pub, err := kafka.NewSyncProducer(c.Brokers, kc)
37 | if err != nil {
38 | panic(err)
39 | }
40 | return pub
41 | }
42 |
43 | func newRedis(c *conf.Redis) *redis.Pool {
44 | return &redis.Pool{
45 | MaxIdle: c.Idle,
46 | MaxActive: c.Active,
47 | IdleTimeout: time.Duration(c.IdleTimeout),
48 | Dial: func() (redis.Conn, error) {
49 | conn, err := redis.Dial(c.Network, c.Addr,
50 | redis.DialConnectTimeout(time.Duration(c.DialTimeout)),
51 | redis.DialReadTimeout(time.Duration(c.ReadTimeout)),
52 | redis.DialWriteTimeout(time.Duration(c.WriteTimeout)),
53 | redis.DialPassword(c.Auth),
54 | )
55 | if err != nil {
56 | return nil, err
57 | }
58 | return conn, nil
59 | },
60 | }
61 | }
62 |
63 | // Close close the resource.
64 | func (d *Dao) Close() error {
65 | return d.redis.Close()
66 | }
67 |
68 | // Ping dao ping.
69 | func (d *Dao) Ping(c context.Context) error {
70 | return d.pingRedis(c)
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/websocket/server.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "crypto/sha1"
5 | "encoding/base64"
6 | "errors"
7 | "io"
8 | "strings"
9 |
10 | "github.com/Terry-Mao/goim/pkg/bufio"
11 | )
12 |
13 | var (
14 | keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
15 | // ErrBadRequestMethod bad request method
16 | ErrBadRequestMethod = errors.New("bad method")
17 | // ErrNotWebSocket not websocket protocal
18 | ErrNotWebSocket = errors.New("not websocket protocol")
19 | // ErrBadWebSocketVersion bad websocket version
20 | ErrBadWebSocketVersion = errors.New("missing or bad WebSocket Version")
21 | // ErrChallengeResponse mismatch challenge response
22 | ErrChallengeResponse = errors.New("mismatch challenge/response")
23 | )
24 |
25 | // Upgrade Switching Protocols
26 | func Upgrade(rwc io.ReadWriteCloser, rr *bufio.Reader, wr *bufio.Writer, req *Request) (conn *Conn, err error) {
27 | if req.Method != "GET" {
28 | return nil, ErrBadRequestMethod
29 | }
30 | if req.Header.Get("Sec-Websocket-Version") != "13" {
31 | return nil, ErrBadWebSocketVersion
32 | }
33 | if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" {
34 | return nil, ErrNotWebSocket
35 | }
36 | if !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") {
37 | return nil, ErrNotWebSocket
38 | }
39 | challengeKey := req.Header.Get("Sec-Websocket-Key")
40 | if challengeKey == "" {
41 | return nil, ErrChallengeResponse
42 | }
43 | _, _ = wr.WriteString("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n")
44 | _, _ = wr.WriteString("Sec-WebSocket-Accept: " + computeAcceptKey(challengeKey) + "\r\n\r\n")
45 | if err = wr.Flush(); err != nil {
46 | return
47 | }
48 | return newConn(rwc, rr, wr), nil
49 | }
50 |
51 | func computeAcceptKey(challengeKey string) string {
52 | h := sha1.New()
53 | _, _ = h.Write([]byte(challengeKey))
54 | _, _ = h.Write(keyGUID)
55 | return base64.StdEncoding.EncodeToString(h.Sum(nil))
56 | }
57 |
--------------------------------------------------------------------------------
/internal/comet/ring.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "github.com/Terry-Mao/goim/api/protocol"
5 | "github.com/Terry-Mao/goim/internal/comet/conf"
6 | "github.com/Terry-Mao/goim/internal/comet/errors"
7 | log "github.com/golang/glog"
8 | )
9 |
10 | // Ring ring proto buffer.
11 | type Ring struct {
12 | // read
13 | rp uint64
14 | num uint64
15 | mask uint64
16 | // TODO split cacheline, many cpu cache line size is 64
17 | // pad [40]byte
18 | // write
19 | wp uint64
20 | data []protocol.Proto
21 | }
22 |
23 | // NewRing new a ring buffer.
24 | func NewRing(num int) *Ring {
25 | r := new(Ring)
26 | r.init(uint64(num))
27 | return r
28 | }
29 |
30 | // Init init ring.
31 | func (r *Ring) Init(num int) {
32 | r.init(uint64(num))
33 | }
34 |
35 | func (r *Ring) init(num uint64) {
36 | // 2^N
37 | if num&(num-1) != 0 {
38 | for num&(num-1) != 0 {
39 | num &= num - 1
40 | }
41 | num <<= 1
42 | }
43 | r.data = make([]protocol.Proto, num)
44 | r.num = num
45 | r.mask = r.num - 1
46 | }
47 |
48 | // Get get a proto from ring.
49 | func (r *Ring) Get() (proto *protocol.Proto, err error) {
50 | if r.rp == r.wp {
51 | return nil, errors.ErrRingEmpty
52 | }
53 | proto = &r.data[r.rp&r.mask]
54 | return
55 | }
56 |
57 | // GetAdv incr read index.
58 | func (r *Ring) GetAdv() {
59 | r.rp++
60 | if conf.Conf.Debug {
61 | log.Infof("ring rp: %d, idx: %d", r.rp, r.rp&r.mask)
62 | }
63 | }
64 |
65 | // Set get a proto to write.
66 | func (r *Ring) Set() (proto *protocol.Proto, err error) {
67 | if r.wp-r.rp >= r.num {
68 | return nil, errors.ErrRingFull
69 | }
70 | proto = &r.data[r.wp&r.mask]
71 | return
72 | }
73 |
74 | // SetAdv incr write index.
75 | func (r *Ring) SetAdv() {
76 | r.wp++
77 | if conf.Conf.Debug {
78 | log.Infof("ring wp: %d, idx: %d", r.wp, r.wp&r.mask)
79 | }
80 | }
81 |
82 | // Reset reset ring.
83 | func (r *Ring) Reset() {
84 | r.rp = 0
85 | r.wp = 0
86 | // prevent pad compiler optimization
87 | // r.pad = [40]byte{}
88 | }
89 |
--------------------------------------------------------------------------------
/internal/comet/room.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/Terry-Mao/goim/api/protocol"
7 | "github.com/Terry-Mao/goim/internal/comet/errors"
8 | )
9 |
10 | // Room is a room and store channel room info.
11 | type Room struct {
12 | ID string
13 | rLock sync.RWMutex
14 | next *Channel
15 | drop bool
16 | Online int32 // dirty read is ok
17 | AllOnline int32
18 | }
19 |
20 | // NewRoom new a room struct, store channel room info.
21 | func NewRoom(id string) (r *Room) {
22 | r = new(Room)
23 | r.ID = id
24 | r.drop = false
25 | r.next = nil
26 | r.Online = 0
27 | return
28 | }
29 |
30 | // Put put channel into the room.
31 | func (r *Room) Put(ch *Channel) (err error) {
32 | r.rLock.Lock()
33 | if !r.drop {
34 | if r.next != nil {
35 | r.next.Prev = ch
36 | }
37 | ch.Next = r.next
38 | ch.Prev = nil
39 | r.next = ch // insert to header
40 | r.Online++
41 | } else {
42 | err = errors.ErrRoomDroped
43 | }
44 | r.rLock.Unlock()
45 | return
46 | }
47 |
48 | // Del delete channel from the room.
49 | func (r *Room) Del(ch *Channel) bool {
50 | r.rLock.Lock()
51 | if ch.Next != nil {
52 | // if not footer
53 | ch.Next.Prev = ch.Prev
54 | }
55 | if ch.Prev != nil {
56 | // if not header
57 | ch.Prev.Next = ch.Next
58 | } else {
59 | r.next = ch.Next
60 | }
61 | ch.Next = nil
62 | ch.Prev = nil
63 | r.Online--
64 | r.drop = r.Online == 0
65 | r.rLock.Unlock()
66 | return r.drop
67 | }
68 |
69 | // Push push msg to the room, if chan full discard it.
70 | func (r *Room) Push(p *protocol.Proto) {
71 | r.rLock.RLock()
72 | for ch := r.next; ch != nil; ch = ch.Next {
73 | _ = ch.Push(p)
74 | }
75 | r.rLock.RUnlock()
76 | }
77 |
78 | // Close close the room.
79 | func (r *Room) Close() {
80 | r.rLock.RLock()
81 | for ch := r.next; ch != nil; ch = ch.Next {
82 | ch.Close()
83 | }
84 | r.rLock.RUnlock()
85 | }
86 |
87 | // OnlineNum the room all online.
88 | func (r *Room) OnlineNum() int32 {
89 | if r.AllOnline > 0 {
90 | return r.AllOnline
91 | }
92 | return r.Online
93 | }
94 |
--------------------------------------------------------------------------------
/internal/logic/nodes.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | pb "github.com/Terry-Mao/goim/api/logic"
8 | "github.com/Terry-Mao/goim/internal/logic/model"
9 | "github.com/bilibili/discovery/naming"
10 | log "github.com/golang/glog"
11 | )
12 |
13 | // NodesInstances get servers info.
14 | func (l *Logic) NodesInstances(c context.Context) (res []*naming.Instance) {
15 | return l.nodes
16 | }
17 |
18 | // NodesWeighted get node list.
19 | func (l *Logic) NodesWeighted(c context.Context, platform, clientIP string) *pb.NodesReply {
20 | reply := &pb.NodesReply{
21 | Domain: l.c.Node.DefaultDomain,
22 | TcpPort: int32(l.c.Node.TCPPort),
23 | WsPort: int32(l.c.Node.WSPort),
24 | WssPort: int32(l.c.Node.WSSPort),
25 | Heartbeat: int32(time.Duration(l.c.Node.Heartbeat) / time.Second),
26 | HeartbeatMax: int32(l.c.Node.HeartbeatMax),
27 | Backoff: &pb.Backoff{
28 | MaxDelay: l.c.Backoff.MaxDelay,
29 | BaseDelay: l.c.Backoff.BaseDelay,
30 | Factor: l.c.Backoff.Factor,
31 | Jitter: l.c.Backoff.Jitter,
32 | },
33 | }
34 | domains, addrs := l.nodeAddrs(c, clientIP)
35 | if platform == model.PlatformWeb {
36 | reply.Nodes = domains
37 | } else {
38 | reply.Nodes = addrs
39 | }
40 | if len(reply.Nodes) == 0 {
41 | reply.Nodes = []string{l.c.Node.DefaultDomain}
42 | }
43 | return reply
44 | }
45 |
46 | func (l *Logic) nodeAddrs(c context.Context, clientIP string) (domains, addrs []string) {
47 | var (
48 | region string
49 | )
50 | province, err := l.location(c, clientIP)
51 | if err == nil {
52 | region = l.regions[province]
53 | }
54 | log.Infof("nodeAddrs clientIP:%s region:%s province:%s domains:%v addrs:%v", clientIP, region, province, domains, addrs)
55 | return l.loadBalancer.NodeAddrs(region, l.c.Node.HostDomain, l.c.Node.RegionWeight)
56 | }
57 |
58 | // location find a geolocation of an IP address including province, region and country.
59 | func (l *Logic) location(c context.Context, clientIP string) (province string, err error) {
60 | // province: config mapping
61 | return
62 | }
63 |
--------------------------------------------------------------------------------
/internal/comet/round.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "github.com/Terry-Mao/goim/internal/comet/conf"
5 | "github.com/Terry-Mao/goim/pkg/bytes"
6 | "github.com/Terry-Mao/goim/pkg/time"
7 | )
8 |
9 | // RoundOptions round options.
10 | type RoundOptions struct {
11 | Timer int
12 | TimerSize int
13 | Reader int
14 | ReadBuf int
15 | ReadBufSize int
16 | Writer int
17 | WriteBuf int
18 | WriteBufSize int
19 | }
20 |
21 | // Round used for connection round-robin get a reader/writer/timer for split big lock.
22 | type Round struct {
23 | readers []bytes.Pool
24 | writers []bytes.Pool
25 | timers []time.Timer
26 | options RoundOptions
27 | }
28 |
29 | // NewRound new a round struct.
30 | func NewRound(c *conf.Config) (r *Round) {
31 | var i int
32 | r = &Round{
33 | options: RoundOptions{
34 | Reader: c.TCP.Reader,
35 | ReadBuf: c.TCP.ReadBuf,
36 | ReadBufSize: c.TCP.ReadBufSize,
37 | Writer: c.TCP.Writer,
38 | WriteBuf: c.TCP.WriteBuf,
39 | WriteBufSize: c.TCP.WriteBufSize,
40 | Timer: c.Protocol.Timer,
41 | TimerSize: c.Protocol.TimerSize,
42 | }}
43 | // reader
44 | r.readers = make([]bytes.Pool, r.options.Reader)
45 | for i = 0; i < r.options.Reader; i++ {
46 | r.readers[i].Init(r.options.ReadBuf, r.options.ReadBufSize)
47 | }
48 | // writer
49 | r.writers = make([]bytes.Pool, r.options.Writer)
50 | for i = 0; i < r.options.Writer; i++ {
51 | r.writers[i].Init(r.options.WriteBuf, r.options.WriteBufSize)
52 | }
53 | // timer
54 | r.timers = make([]time.Timer, r.options.Timer)
55 | for i = 0; i < r.options.Timer; i++ {
56 | r.timers[i].Init(r.options.TimerSize)
57 | }
58 | return
59 | }
60 |
61 | // Timer get a timer.
62 | func (r *Round) Timer(rn int) *time.Timer {
63 | return &(r.timers[rn%r.options.Timer])
64 | }
65 |
66 | // Reader get a reader memory buffer.
67 | func (r *Round) Reader(rn int) *bytes.Pool {
68 | return &(r.readers[rn%r.options.Reader])
69 | }
70 |
71 | // Writer get a writer memory buffer pool.
72 | func (r *Round) Writer(rn int) *bytes.Pool {
73 | return &(r.writers[rn%r.options.Writer])
74 | }
75 |
--------------------------------------------------------------------------------
/internal/comet/channel.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/Terry-Mao/goim/api/protocol"
7 | "github.com/Terry-Mao/goim/internal/comet/errors"
8 | "github.com/Terry-Mao/goim/pkg/bufio"
9 | )
10 |
11 | // Channel used by message pusher send msg to write goroutine.
12 | type Channel struct {
13 | Room *Room
14 | CliProto Ring
15 | signal chan *protocol.Proto
16 | Writer bufio.Writer
17 | Reader bufio.Reader
18 | Next *Channel
19 | Prev *Channel
20 |
21 | Mid int64
22 | Key string
23 | IP string
24 | watchOps map[int32]struct{}
25 | mutex sync.RWMutex
26 | }
27 |
28 | // NewChannel new a channel.
29 | func NewChannel(cli, svr int) *Channel {
30 | c := new(Channel)
31 | c.CliProto.Init(cli)
32 | c.signal = make(chan *protocol.Proto, svr)
33 | c.watchOps = make(map[int32]struct{})
34 | return c
35 | }
36 |
37 | // Watch watch a operation.
38 | func (c *Channel) Watch(accepts ...int32) {
39 | c.mutex.Lock()
40 | for _, op := range accepts {
41 | c.watchOps[op] = struct{}{}
42 | }
43 | c.mutex.Unlock()
44 | }
45 |
46 | // UnWatch unwatch an operation
47 | func (c *Channel) UnWatch(accepts ...int32) {
48 | c.mutex.Lock()
49 | for _, op := range accepts {
50 | delete(c.watchOps, op)
51 | }
52 | c.mutex.Unlock()
53 | }
54 |
55 | // NeedPush verify if in watch.
56 | func (c *Channel) NeedPush(op int32) bool {
57 | c.mutex.RLock()
58 | if _, ok := c.watchOps[op]; ok {
59 | c.mutex.RUnlock()
60 | return true
61 | }
62 | c.mutex.RUnlock()
63 | return false
64 | }
65 |
66 | // Push server push message.
67 | func (c *Channel) Push(p *protocol.Proto) (err error) {
68 | select {
69 | case c.signal <- p:
70 | default:
71 | err = errors.ErrSignalFullMsgDropped
72 | }
73 | return
74 | }
75 |
76 | // Ready check the channel ready or close?
77 | func (c *Channel) Ready() *protocol.Proto {
78 | return <-c.signal
79 | }
80 |
81 | // Signal send signal to the channel, protocol ready.
82 | func (c *Channel) Signal() {
83 | c.signal <- protocol.ProtoReady
84 | }
85 |
86 | // Close close the channel.
87 | func (c *Channel) Close() {
88 | c.signal <- protocol.ProtoFinish
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/strings/ints.go:
--------------------------------------------------------------------------------
1 | package strings
2 |
3 | import (
4 | "bytes"
5 | "strconv"
6 | "strings"
7 | "sync"
8 | )
9 |
10 | var (
11 | bfPool = sync.Pool{
12 | New: func() interface{} {
13 | return bytes.NewBuffer([]byte{})
14 | },
15 | }
16 | )
17 |
18 | // JoinInt32s format int32 slice like:n1,n2,n3.
19 | func JoinInt32s(is []int32, p string) string {
20 | if len(is) == 0 {
21 | return ""
22 | }
23 | if len(is) == 1 {
24 | return strconv.FormatInt(int64(is[0]), 10)
25 | }
26 | buf := bfPool.Get().(*bytes.Buffer)
27 | for _, i := range is {
28 | buf.WriteString(strconv.FormatInt(int64(i), 10))
29 | buf.WriteString(p)
30 | }
31 | if buf.Len() > 0 {
32 | buf.Truncate(buf.Len() - 1)
33 | }
34 | s := buf.String()
35 | buf.Reset()
36 | bfPool.Put(buf)
37 | return s
38 | }
39 |
40 | // SplitInt32s split string into int32 slice.
41 | func SplitInt32s(s, p string) ([]int32, error) {
42 | if s == "" {
43 | return nil, nil
44 | }
45 | sArr := strings.Split(s, p)
46 | res := make([]int32, 0, len(sArr))
47 | for _, sc := range sArr {
48 | i, err := strconv.ParseInt(sc, 10, 32)
49 | if err != nil {
50 | return nil, err
51 | }
52 | res = append(res, int32(i))
53 | }
54 | return res, nil
55 | }
56 |
57 | // JoinInt64s format int64 slice like:n1,n2,n3.
58 | func JoinInt64s(is []int64, p string) string {
59 | if len(is) == 0 {
60 | return ""
61 | }
62 | if len(is) == 1 {
63 | return strconv.FormatInt(is[0], 10)
64 | }
65 | buf := bfPool.Get().(*bytes.Buffer)
66 | for _, i := range is {
67 | buf.WriteString(strconv.FormatInt(i, 10))
68 | buf.WriteString(p)
69 | }
70 | if buf.Len() > 0 {
71 | buf.Truncate(buf.Len() - 1)
72 | }
73 | s := buf.String()
74 | buf.Reset()
75 | bfPool.Put(buf)
76 | return s
77 | }
78 |
79 | // SplitInt64s split string into int64 slice.
80 | func SplitInt64s(s, p string) ([]int64, error) {
81 | if s == "" {
82 | return nil, nil
83 | }
84 | sArr := strings.Split(s, p)
85 | res := make([]int64, 0, len(sArr))
86 | for _, sc := range sArr {
87 | i, err := strconv.ParseInt(sc, 10, 64)
88 | if err != nil {
89 | return nil, err
90 | }
91 | res = append(res, i)
92 | }
93 | return res, nil
94 | }
95 |
--------------------------------------------------------------------------------
/internal/logic/balancer_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "sort"
5 | "testing"
6 |
7 | "github.com/bilibili/discovery/naming"
8 | "github.com/Terry-Mao/goim/internal/logic/model"
9 | )
10 |
11 | func TestWeightedNode(t *testing.T) {
12 | nodes := []*weightedNode{
13 | &weightedNode{fixedWeight: 1, currentWeight: 1, currentConns: 1000000},
14 | &weightedNode{fixedWeight: 2, currentWeight: 1, currentConns: 1000000},
15 | &weightedNode{fixedWeight: 3, currentWeight: 1, currentConns: 1000000},
16 | }
17 | for i := 0; i < 100; i++ {
18 | for _, n := range nodes {
19 | n.calculateWeight(6, nodes[0].currentConns+nodes[1].currentConns+nodes[2].currentConns, 1.0)
20 | }
21 | sort.Slice(nodes, func(i, j int) bool {
22 | return nodes[i].currentWeight > nodes[j].currentWeight
23 | })
24 | nodes[0].chosen()
25 | }
26 | ft := float64(nodes[0].fixedWeight + nodes[1].fixedWeight + nodes[2].fixedWeight)
27 | ct := float64(nodes[0].currentConns + nodes[1].currentConns + nodes[2].currentConns)
28 | for _, n := range nodes {
29 | t.Logf("match ratio %d:%d", int(float64(n.fixedWeight)/ft*100*0.6), int(float64(n.currentConns)/ct*100))
30 | }
31 | }
32 |
33 | func TestLoadBalancer(t *testing.T) {
34 | ss := []*naming.Instance{
35 | &naming.Instance{
36 | Region: "bj",
37 | Hostname: "01",
38 | Metadata: map[string]string{
39 | model.MetaWeight: "10",
40 | model.MetaConnCount: "240590",
41 | model.MetaIPCount: "10",
42 | model.MetaAddrs: "ip_bj",
43 | },
44 | },
45 | &naming.Instance{
46 | Region: "sh",
47 | Hostname: "02",
48 | Metadata: map[string]string{
49 | model.MetaWeight: "10",
50 | model.MetaConnCount: "375420",
51 | model.MetaIPCount: "10",
52 | model.MetaAddrs: "ip_sh",
53 | },
54 | },
55 | &naming.Instance{
56 | Region: "gz",
57 | Hostname: "03",
58 | Metadata: map[string]string{
59 | model.MetaWeight: "10",
60 | model.MetaConnCount: "293430",
61 | model.MetaIPCount: "10",
62 | model.MetaAddrs: "ip_gz",
63 | },
64 | },
65 | }
66 | lb := NewLoadBalancer()
67 | lb.Update(ss)
68 | for i := 0; i < 5; i++ {
69 | t.Log(lb.NodeAddrs("sh", ".test", 1.6))
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/scripts/zk.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # config parameters
3 | JDK_HOME=/usr/local/jdk8
4 | ZK_HOME=/data/server/zookeeper
5 | ZK_SUPERVISOR=/etc/supervisor/conf.d/zookeeper.conf
6 |
7 | USAGE="./zk.sh {zk.id} {zk.addr1,zk.addr2,zk.addr3}\r\neg:./zk.sh 1 127.0.0.1:2181,127.0.0.2:2181,127.0.0.3:2181"
8 | # check parammeters
9 | ZK_ID=$1
10 | str=$2
11 | ZK_ADDRS=(${str//,/ })
12 | ZK_ADDRS_SIZE=${#ZK_ADDRS[@]}
13 | if [ ! -n "$1" ];then
14 | echo -e $USAGE
15 | exit 1
16 | fi
17 | if [ ! -n "$2" ];then
18 | echo -e $USAGE
19 | exit 1
20 | fi
21 | echo $ZK_ID
22 | echo ${ZK_ADDRS[@]}
23 |
24 |
25 | set -e
26 |
27 | curl -L http://apache.website-solution.net/zookeeper/zookeeper-3.4.12/zookeeper-3.4.12.tar.gz -o zookeeper-3.4.12.tar.gz
28 | tar zxf zookeeper-3.4.12.tar.gz
29 | mkdir -p $ZK_HOME
30 | echo $ZK_ID>$ZK_HOME/myid
31 | mv zookeeper-3.4.12/* $ZK_HOME
32 | echo "tickTime=2000" > $ZK_HOME/conf/zoo.cfg
33 | echo "initLimit=10" >> $ZK_HOME/conf/zoo.cfg
34 | echo "syncLimit=5" >> $ZK_HOME/conf/zoo.cfg
35 | echo "dataDir=/data/server/zookeeper" >> $ZK_HOME/conf/zoo.cfg
36 | echo "clientPort=2181" >> $ZK_HOME/conf/zoo.cfg
37 | echo "autopurge.snapRetainCount=5" >> $ZK_HOME/conf/zoo.cfg
38 | echo "autopurge.purgeInterval=24" >> $ZK_HOME/conf/zoo.cfg
39 | for ((index=0;index<$ZK_ADDRS_SIZE;index++))
40 | do
41 | id=$[$index+1]
42 | echo "server.$id=${ZK_ADDRS[index]}:2888:3888" >> $ZK_HOME/conf/zoo.cfg
43 | done
44 |
45 | # install supervisor
46 | if ! type "supervisorctl" > /dev/null; then
47 | apt-get install -y supervisor
48 | fi
49 | # zookeeper supervisor config
50 | mkdir -p /etc/supervisor/conf.d/
51 | mkdir -p /data/log/zookeeper/
52 | echo "[program:zookeeper]
53 | command=${ZK_HOME}/bin/zkServer.sh start-foreground
54 | directory=${ZK_HOME}
55 | user=root
56 | autostart=true
57 | autorestart=true
58 | exitcodes=0
59 | startsecs=10
60 | startretries=10
61 | stopwaitsecs=10
62 | stopsignal=KILL
63 | stdout_logfile=/data/log/zookeeper/stdout.log
64 | stderr_logfile=/data/log/zookeeper/stderr.log
65 | stdout_logfile_maxbytes=100MB
66 | stdout_logfile_backups=5
67 | stderr_logfile_maxbytes=100MB
68 | stderr_logfile_backups=5
69 | environment=JAVA_HOME=$JDK_HOME,JRE_HOME='$JDK_HOME/jre'
70 | ">$ZK_SUPERVISOR
71 |
72 | supervisorctl update
73 |
--------------------------------------------------------------------------------
/internal/logic/dao/kafka.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "context"
5 | "strconv"
6 |
7 | pb "github.com/Terry-Mao/goim/api/logic"
8 | log "github.com/golang/glog"
9 | "github.com/golang/protobuf/proto"
10 | sarama "gopkg.in/Shopify/sarama.v1"
11 | )
12 |
13 | // PushMsg push a message to databus.
14 | func (d *Dao) PushMsg(c context.Context, op int32, server string, keys []string, msg []byte) (err error) {
15 | pushMsg := &pb.PushMsg{
16 | Type: pb.PushMsg_PUSH,
17 | Operation: op,
18 | Server: server,
19 | Keys: keys,
20 | Msg: msg,
21 | }
22 | b, err := proto.Marshal(pushMsg)
23 | if err != nil {
24 | return
25 | }
26 | m := &sarama.ProducerMessage{
27 | Key: sarama.StringEncoder(keys[0]),
28 | Topic: d.c.Kafka.Topic,
29 | Value: sarama.ByteEncoder(b),
30 | }
31 | if _, _, err = d.kafkaPub.SendMessage(m); err != nil {
32 | log.Errorf("PushMsg.send(push pushMsg:%v) error(%v)", pushMsg, err)
33 | }
34 | return
35 | }
36 |
37 | // BroadcastRoomMsg push a message to databus.
38 | func (d *Dao) BroadcastRoomMsg(c context.Context, op int32, room string, msg []byte) (err error) {
39 | pushMsg := &pb.PushMsg{
40 | Type: pb.PushMsg_ROOM,
41 | Operation: op,
42 | Room: room,
43 | Msg: msg,
44 | }
45 | b, err := proto.Marshal(pushMsg)
46 | if err != nil {
47 | return
48 | }
49 | m := &sarama.ProducerMessage{
50 | Key: sarama.StringEncoder(room),
51 | Topic: d.c.Kafka.Topic,
52 | Value: sarama.ByteEncoder(b),
53 | }
54 | if _, _, err = d.kafkaPub.SendMessage(m); err != nil {
55 | log.Errorf("PushMsg.send(broadcast_room pushMsg:%v) error(%v)", pushMsg, err)
56 | }
57 | return
58 | }
59 |
60 | // BroadcastMsg push a message to databus.
61 | func (d *Dao) BroadcastMsg(c context.Context, op, speed int32, msg []byte) (err error) {
62 | pushMsg := &pb.PushMsg{
63 | Type: pb.PushMsg_BROADCAST,
64 | Operation: op,
65 | Speed: speed,
66 | Msg: msg,
67 | }
68 | b, err := proto.Marshal(pushMsg)
69 | if err != nil {
70 | return
71 | }
72 | m := &sarama.ProducerMessage{
73 | Key: sarama.StringEncoder(strconv.FormatInt(int64(op), 10)),
74 | Topic: d.c.Kafka.Topic,
75 | Value: sarama.ByteEncoder(b),
76 | }
77 | if _, _, err = d.kafkaPub.SendMessage(m); err != nil {
78 | log.Errorf("PushMsg.send(broadcast pushMsg:%v) error(%v)", pushMsg, err)
79 | }
80 | return
81 | }
82 |
--------------------------------------------------------------------------------
/cmd/logic/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "net"
7 | "os"
8 | "os/signal"
9 | "strconv"
10 | "syscall"
11 |
12 | "github.com/bilibili/discovery/naming"
13 | resolver "github.com/bilibili/discovery/naming/grpc"
14 | "github.com/Terry-Mao/goim/internal/logic"
15 | "github.com/Terry-Mao/goim/internal/logic/conf"
16 | "github.com/Terry-Mao/goim/internal/logic/grpc"
17 | "github.com/Terry-Mao/goim/internal/logic/http"
18 | "github.com/Terry-Mao/goim/internal/logic/model"
19 | "github.com/Terry-Mao/goim/pkg/ip"
20 | log "github.com/golang/glog"
21 | )
22 |
23 | const (
24 | ver = "2.0.0"
25 | appid = "goim.logic"
26 | )
27 |
28 | func main() {
29 | flag.Parse()
30 | if err := conf.Init(); err != nil {
31 | panic(err)
32 | }
33 | log.Infof("goim-logic [version: %s env: %+v] start", ver, conf.Conf.Env)
34 | // grpc register naming
35 | dis := naming.New(conf.Conf.Discovery)
36 | resolver.Register(dis)
37 | // logic
38 | srv := logic.New(conf.Conf)
39 | httpSrv := http.New(conf.Conf.HTTPServer, srv)
40 | rpcSrv := grpc.New(conf.Conf.RPCServer, srv)
41 | cancel := register(dis, srv)
42 | // signal
43 | c := make(chan os.Signal, 1)
44 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
45 | for {
46 | s := <-c
47 | log.Infof("goim-logic get a signal %s", s.String())
48 | switch s {
49 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
50 | if cancel != nil {
51 | cancel()
52 | }
53 | srv.Close()
54 | httpSrv.Close()
55 | rpcSrv.GracefulStop()
56 | log.Infof("goim-logic [version: %s] exit", ver)
57 | log.Flush()
58 | return
59 | case syscall.SIGHUP:
60 | default:
61 | return
62 | }
63 | }
64 | }
65 |
66 | func register(dis *naming.Discovery, srv *logic.Logic) context.CancelFunc {
67 | env := conf.Conf.Env
68 | addr := ip.InternalIP()
69 | _, port, _ := net.SplitHostPort(conf.Conf.RPCServer.Addr)
70 | ins := &naming.Instance{
71 | Region: env.Region,
72 | Zone: env.Zone,
73 | Env: env.DeployEnv,
74 | Hostname: env.Host,
75 | AppID: appid,
76 | Addrs: []string{
77 | "grpc://" + addr + ":" + port,
78 | },
79 | Metadata: map[string]string{
80 | model.MetaWeight: strconv.FormatInt(env.Weight, 10),
81 | },
82 | }
83 | cancel, err := dis.Register(ins)
84 | if err != nil {
85 | panic(err)
86 | }
87 | return cancel
88 | }
89 |
--------------------------------------------------------------------------------
/internal/job/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "time"
7 |
8 | "github.com/bilibili/discovery/naming"
9 | "github.com/BurntSushi/toml"
10 | xtime "github.com/Terry-Mao/goim/pkg/time"
11 | )
12 |
13 | var (
14 | confPath string
15 | region string
16 | zone string
17 | deployEnv string
18 | host string
19 | // Conf config
20 | Conf *Config
21 | )
22 |
23 | func init() {
24 | var (
25 | defHost, _ = os.Hostname()
26 | )
27 | flag.StringVar(&confPath, "conf", "job-example.toml", "default config path")
28 | flag.StringVar(®ion, "region", os.Getenv("REGION"), "avaliable region. or use REGION env variable, value: sh etc.")
29 | flag.StringVar(&zone, "zone", os.Getenv("ZONE"), "avaliable zone. or use ZONE env variable, value: sh001/sh002 etc.")
30 | flag.StringVar(&deployEnv, "deploy.env", os.Getenv("DEPLOY_ENV"), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.")
31 | flag.StringVar(&host, "host", defHost, "machine hostname. or use default machine hostname.")
32 | }
33 |
34 | // Init init config.
35 | func Init() (err error) {
36 | Conf = Default()
37 | _, err = toml.DecodeFile(confPath, &Conf)
38 | return
39 | }
40 |
41 | // Default new a config with specified defualt value.
42 | func Default() *Config {
43 | return &Config{
44 | Env: &Env{Region: region, Zone: zone, DeployEnv: deployEnv, Host: host},
45 | Discovery: &naming.Config{Region: region, Zone: zone, Env: deployEnv, Host: host},
46 | Comet: &Comet{RoutineChan: 1024, RoutineSize: 32},
47 | Room: &Room{
48 | Batch: 20,
49 | Signal: xtime.Duration(time.Second),
50 | Idle: xtime.Duration(time.Minute * 15),
51 | },
52 | }
53 | }
54 |
55 | // Config is job config.
56 | type Config struct {
57 | Env *Env
58 | Kafka *Kafka
59 | Discovery *naming.Config
60 | Comet *Comet
61 | Room *Room
62 | }
63 |
64 | // Room is room config.
65 | type Room struct {
66 | Batch int
67 | Signal xtime.Duration
68 | Idle xtime.Duration
69 | }
70 |
71 | // Comet is comet config.
72 | type Comet struct {
73 | RoutineChan int
74 | RoutineSize int
75 | }
76 |
77 | // Kafka is kafka config.
78 | type Kafka struct {
79 | Topic string
80 | Group string
81 | Brokers []string
82 | }
83 |
84 | // Env is env config.
85 | type Env struct {
86 | Region string
87 | Zone string
88 | DeployEnv string
89 | Host string
90 | }
91 |
--------------------------------------------------------------------------------
/internal/logic/http/push.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "context"
5 | "io/ioutil"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | func (s *Server) pushKeys(c *gin.Context) {
11 | var arg struct {
12 | Op int32 `form:"operation"`
13 | Keys []string `form:"keys"`
14 | }
15 | if err := c.BindQuery(&arg); err != nil {
16 | errors(c, RequestErr, err.Error())
17 | return
18 | }
19 | // read message
20 | msg, err := ioutil.ReadAll(c.Request.Body)
21 | if err != nil {
22 | errors(c, RequestErr, err.Error())
23 | return
24 | }
25 | if err = s.logic.PushKeys(context.TODO(), arg.Op, arg.Keys, msg); err != nil {
26 | result(c, nil, RequestErr)
27 | return
28 | }
29 | result(c, nil, OK)
30 | }
31 |
32 | func (s *Server) pushMids(c *gin.Context) {
33 | var arg struct {
34 | Op int32 `form:"operation"`
35 | Mids []int64 `form:"mids"`
36 | }
37 | if err := c.BindQuery(&arg); err != nil {
38 | errors(c, RequestErr, err.Error())
39 | return
40 | }
41 | // read message
42 | msg, err := ioutil.ReadAll(c.Request.Body)
43 | if err != nil {
44 | errors(c, RequestErr, err.Error())
45 | return
46 | }
47 | if err = s.logic.PushMids(context.TODO(), arg.Op, arg.Mids, msg); err != nil {
48 | errors(c, ServerErr, err.Error())
49 | return
50 | }
51 | result(c, nil, OK)
52 | }
53 |
54 | func (s *Server) pushRoom(c *gin.Context) {
55 | var arg struct {
56 | Op int32 `form:"operation" binding:"required"`
57 | Type string `form:"type" binding:"required"`
58 | Room string `form:"room" binding:"required"`
59 | }
60 | if err := c.BindQuery(&arg); err != nil {
61 | errors(c, RequestErr, err.Error())
62 | return
63 | }
64 | // read message
65 | msg, err := ioutil.ReadAll(c.Request.Body)
66 | if err != nil {
67 | errors(c, RequestErr, err.Error())
68 | return
69 | }
70 | if err = s.logic.PushRoom(c, arg.Op, arg.Type, arg.Room, msg); err != nil {
71 | errors(c, ServerErr, err.Error())
72 | return
73 | }
74 | result(c, nil, OK)
75 | }
76 |
77 | func (s *Server) pushAll(c *gin.Context) {
78 | var arg struct {
79 | Op int32 `form:"operation" binding:"required"`
80 | Speed int32 `form:"speed"`
81 | }
82 | if err := c.BindQuery(&arg); err != nil {
83 | errors(c, RequestErr, err.Error())
84 | return
85 | }
86 | msg, err := ioutil.ReadAll(c.Request.Body)
87 | if err != nil {
88 | errors(c, RequestErr, err.Error())
89 | return
90 | }
91 | if err = s.logic.PushAll(c, arg.Op, arg.Speed, msg); err != nil {
92 | errors(c, ServerErr, err.Error())
93 | return
94 | }
95 | result(c, nil, OK)
96 | }
97 |
--------------------------------------------------------------------------------
/api/logic/logic.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package goim.logic;
4 |
5 | option go_package = "github.com/Terry-Mao/goim/api/logic;logic";
6 |
7 | import "github.com/Terry-Mao/goim/api/protocol/protocol.proto";
8 |
9 | message PushMsg {
10 | enum Type {
11 | PUSH = 0;
12 | ROOM = 1;
13 | BROADCAST = 2;
14 | }
15 | Type type = 1;
16 | int32 operation = 2;
17 | int32 speed = 3;
18 | string server = 4;
19 | string room = 5;
20 | repeated string keys = 6;
21 | bytes msg = 7;
22 | }
23 |
24 | message ConnectReq {
25 | string server = 1;
26 | string cookie = 2;
27 | bytes token = 3;
28 | }
29 |
30 | message ConnectReply {
31 | int64 mid = 1;
32 | string key = 2;
33 | string roomID = 3;
34 | repeated int32 accepts = 4;
35 | int64 heartbeat = 5;
36 | }
37 |
38 | message DisconnectReq {
39 | int64 mid = 1;
40 | string key = 2;
41 | string server = 3;
42 | }
43 |
44 | message DisconnectReply {
45 | bool has = 1;
46 | }
47 |
48 | message HeartbeatReq {
49 | int64 mid = 1;
50 | string key = 2;
51 | string server = 3;
52 | }
53 |
54 | message HeartbeatReply {
55 | }
56 |
57 | message OnlineReq {
58 | string server = 1;
59 | map roomCount = 2;
60 | }
61 |
62 | message OnlineReply {
63 | map allRoomCount = 1;
64 | }
65 |
66 | message ReceiveReq {
67 | int64 mid = 1;
68 | goim.protocol.Proto proto = 2;
69 | }
70 |
71 | message ReceiveReply {
72 | }
73 |
74 | message NodesReq {
75 | string platform = 1;
76 | string clientIP = 2;
77 | }
78 |
79 | message NodesReply {
80 | string domain = 1;
81 | int32 tcp_port = 2;
82 | int32 ws_port = 3;
83 | int32 wss_port = 4;
84 | int32 heartbeat = 5;
85 | repeated string nodes = 6;
86 | Backoff backoff = 7;
87 | int32 heartbeat_max = 8;
88 | }
89 |
90 | message Backoff {
91 | int32 max_delay = 1;
92 | int32 base_delay = 2;
93 | float factor = 3;
94 | float jitter = 4;
95 | }
96 |
97 | service Logic {
98 | // Connect
99 | rpc Connect(ConnectReq) returns (ConnectReply);
100 | // Disconnect
101 | rpc Disconnect(DisconnectReq) returns (DisconnectReply);
102 | // Heartbeat
103 | rpc Heartbeat(HeartbeatReq) returns (HeartbeatReply);
104 | // RenewOnline
105 | rpc RenewOnline(OnlineReq) returns (OnlineReply);
106 | // Receive
107 | rpc Receive(ReceiveReq) returns (ReceiveReply);
108 | //ServerList
109 | rpc Nodes(NodesReq) returns (NodesReply);
110 | }
111 |
--------------------------------------------------------------------------------
/benchmarks/push_rooms/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Start Command eg : ./push_rooms 0 20000 localhost:7172 40
4 | // param 1 : the start of room number
5 | // param 2 : the end of room number
6 | // param 3 : comet server tcp address
7 | // param 4 : push amount each goroutines per second
8 |
9 | import (
10 | "bytes"
11 | "fmt"
12 | "io/ioutil"
13 | "log"
14 | "net"
15 | "net/http"
16 | "os"
17 | "runtime"
18 | "strconv"
19 | "time"
20 | )
21 |
22 | var (
23 | httpClient *http.Client
24 | )
25 |
26 | const testContent = "{\"test\":1}"
27 |
28 | func init() {
29 | httpTransport := &http.Transport{
30 | Dial: func(netw, addr string) (net.Conn, error) {
31 | deadline := time.Now().Add(30 * time.Second)
32 | c, err := net.DialTimeout(netw, addr, 20*time.Second)
33 | if err != nil {
34 | return nil, err
35 | }
36 | _ = c.SetDeadline(deadline)
37 | return c, nil
38 | },
39 | DisableKeepAlives: false,
40 | }
41 | httpClient = &http.Client{
42 | Transport: httpTransport,
43 | }
44 | }
45 |
46 | func main() {
47 | runtime.GOMAXPROCS(runtime.NumCPU())
48 | begin, err := strconv.Atoi(os.Args[1])
49 | if err != nil {
50 | panic(err)
51 | }
52 | length, err := strconv.Atoi(os.Args[2])
53 | if err != nil {
54 | panic(err)
55 | }
56 |
57 | num, err := strconv.Atoi(os.Args[4])
58 | if err != nil {
59 | panic(err)
60 | }
61 | delay := (1000 * time.Millisecond) / time.Duration(num)
62 |
63 | routines := runtime.NumCPU() * 2
64 | log.Printf("start routine num:%d", routines)
65 |
66 | l := length / routines
67 | b, e := begin, begin+l
68 | for i := 0; i < routines; i++ {
69 | go startPush(b, e, delay)
70 | b += l
71 | e += l
72 | }
73 | if b < begin+length {
74 | go startPush(b, begin+length, delay)
75 | }
76 |
77 | time.Sleep(9999 * time.Hour)
78 | }
79 |
80 | func startPush(b, e int, delay time.Duration) {
81 | log.Printf("start Push from %d to %d", b, e)
82 |
83 | for {
84 | for i := b; i < e; i++ {
85 | resp, err := http.Post(fmt.Sprintf("http://%s/goim/push/room?operation=1000&type=test&room=%d", os.Args[3], i), "application/json", bytes.NewBufferString(testContent))
86 | if err != nil {
87 | log.Printf("post error (%v)", err)
88 | continue
89 | }
90 |
91 | body, err := ioutil.ReadAll(resp.Body)
92 | if err != nil {
93 | log.Printf("post error (%v)", err)
94 | return
95 | }
96 | resp.Body.Close()
97 |
98 | log.Printf("push room:%d response %s", i, string(body))
99 | time.Sleep(delay)
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/scripts/kafka.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # config parameters
3 | JDK_HOME=/usr/local/jdk8/
4 | KAFKA_HOME=/data/app/kafka/
5 | KAFKA_CONF=/data/app/kafka/config/server.properties
6 | KAFKA_DATA=/data/kafka_data/
7 | KAFKA_SUPERVISOR=/etc/supervisor/conf.d/kafka.conf
8 |
9 | USAGE="./kafka.sh {broker.id} {current.addr} {zk.addr1,zk.addr2,zk.addr3}\r\neg: ./kafka.sh 1 127.0.0.1 127.0.0.1:2181,127.0.0.2:2181,127.0.0.3:2181"
10 | # check parammeters
11 | BROKER_ID=$1
12 | CURRENT_ADDR=$2
13 | ZK_ADDRS=$3
14 | if [ ! "$BROKER_ID" ]; then
15 | echo "kafka {broker.id} can not be null"
16 | echo -e $USAGE
17 | exit 1
18 | fi
19 | if [ ! "$CURRENT_ADDR" ]; then
20 | echo "current addr can not be null"
21 | exit 1
22 | fi
23 | if [ ! "$ZK_ADDRS" ]; then
24 | echo "zookeeper addrs can not be null"
25 | exit 1
26 | fi
27 | if [ ! -d "$JDK_HOME" ]; then
28 | echo "jdk not exist down jdk8"
29 | ./jdk8.sh
30 | fi
31 |
32 | # download kafka
33 | curl "http://apache.stu.edu.tw/kafka/2.1.0/kafka_2.11-2.1.0.tgz" -o kafka_2.11-2.1.0.tgz
34 | tar zxf kafka_2.11-2.1.0.tgz
35 | # install kafka
36 | mkdir -p $KAFKA_HOME
37 | mkdir -p $KAFKA_DATA
38 | mv kafka_2.11-2.1.0/* $KAFKA_HOME
39 | # kafka config
40 | echo "broker.id=$BROKER_ID">$KAFKA_CONF
41 | echo "host.name=$CURRENT_ADDR">>$KAFKA_CONF
42 | echo "zookeeper.connect=$ZK_ADDRS/kafka">>$KAFKA_CONF
43 | echo "num.network.threads=4
44 | num.io.threads=4
45 | socket.send.buffer.bytes=1024000
46 | socket.receive.buffer.bytes=1024000
47 | socket.request.max.bytes=52428800
48 | log.dirs=$KAFKA_DATA
49 | num.partitions=9
50 | num.recovery.threads.per.data.dir=1
51 | log.cleanup.policy=delete
52 | log.retention.hours=24
53 | log.segment.bytes=536870912
54 | log.retention.check.interval.ms=300000
55 | log.cleaner.enable=false
56 | zookeeper.connection.timeout.ms=6000
57 | default.replication.factor=2
58 | delete.topic.enable=false
59 | auto.create.topics.enable=false">>$KAFKA_CONF
60 |
61 | # install supervisor
62 | if ! type "supervisorctl" > /dev/null; then
63 | apt-get install -y supervisor
64 | fi
65 | # kafka supervisor config
66 | mkdir -p /etc/supervisor/conf.d/
67 | mkdir -p /data/log/kafka/
68 | echo "[program:kafka]
69 | command=$KAFKA_HOME/bin/kafka-server-start.sh $KAFKA_CONF
70 | user=root
71 | autostart=true
72 | autorestart=true
73 | exitcodes=0
74 | startsecs=10
75 | startretries=10
76 | stopwaitsecs=10
77 | stopsignal=KILL
78 | stdout_logfile=/data/log/kafka/stdout.log
79 | stderr_logfile=/data/log/kafka/stderr.log
80 | stdout_logfile_maxbytes=100MB
81 | stdout_logfile_backups=5
82 | stderr_logfile_maxbytes=100MB
83 | stderr_logfile_backups=5
84 | environment=JAVA_HOME=$JDK_HOME,JRE_HOME='$JDK_HOME/jre',KAFKA_HEAP_OPTS='-Xmx6g -Xms6g -XX:MetaspaceSize=96m -XX:G1HeapRegionSize=16M -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80'">$_KAFKA_SUPERVISOR
85 |
86 | supervisorctl update
87 |
--------------------------------------------------------------------------------
/pkg/websocket/request.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "strings"
8 |
9 | "github.com/Terry-Mao/goim/pkg/bufio"
10 | )
11 |
12 | // Request request.
13 | type Request struct {
14 | Method string
15 | RequestURI string
16 | Proto string
17 | Host string
18 | Header http.Header
19 |
20 | reader *bufio.Reader
21 | }
22 |
23 | // ReadRequest reads and parses an incoming request from b.
24 | func ReadRequest(r *bufio.Reader) (req *Request, err error) {
25 | var (
26 | b []byte
27 | ok bool
28 | )
29 | req = &Request{reader: r}
30 | if b, err = req.readLine(); err != nil {
31 | return
32 | }
33 | if req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(string(b)); !ok {
34 | return nil, fmt.Errorf("malformed HTTP request %s", b)
35 | }
36 | if req.Header, err = req.readMIMEHeader(); err != nil {
37 | return
38 | }
39 | req.Host = req.Header.Get("Host")
40 | return req, nil
41 | }
42 |
43 | func (r *Request) readLine() ([]byte, error) {
44 | var line []byte
45 | for {
46 | l, more, err := r.reader.ReadLine()
47 | if err != nil {
48 | return nil, err
49 | }
50 | // Avoid the copy if the first call produced a full line.
51 | if line == nil && !more {
52 | return l, nil
53 | }
54 | line = append(line, l...)
55 | if !more {
56 | break
57 | }
58 | }
59 | return line, nil
60 | }
61 |
62 | func (r *Request) readMIMEHeader() (header http.Header, err error) {
63 | var (
64 | line []byte
65 | i int
66 | k, v string
67 | )
68 | header = make(http.Header, 16)
69 | for {
70 | if line, err = r.readLine(); err != nil {
71 | return
72 | }
73 | line = trim(line)
74 | if len(line) == 0 {
75 | return
76 | }
77 | if i = bytes.IndexByte(line, ':'); i <= 0 {
78 | err = fmt.Errorf("malformed MIME header line: " + string(line))
79 | return
80 | }
81 | k = string(line[:i])
82 | // Skip initial spaces in value.
83 | i++ // skip colon
84 | for i < len(line) && (line[i] == ' ' || line[i] == '\t') {
85 | i++
86 | }
87 | v = string(line[i:])
88 | header.Add(k, v)
89 | }
90 | }
91 |
92 | // parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
93 | func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
94 | s1 := strings.Index(line, " ")
95 | s2 := strings.Index(line[s1+1:], " ")
96 | if s1 < 0 || s2 < 0 {
97 | return
98 | }
99 | s2 += s1 + 1
100 | return line[:s1], line[s1+1 : s2], line[s2+1:], true
101 | }
102 |
103 | // trim returns s with leading and trailing spaces and tabs removed.
104 | // It does not assume Unicode or UTF-8.
105 | func trim(s []byte) []byte {
106 | i := 0
107 | for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
108 | i++
109 | }
110 | n := len(s)
111 | for n > i && (s[n-1] == ' ' || s[n-1] == '\t') {
112 | n--
113 | }
114 | return s[i:n]
115 | }
116 |
--------------------------------------------------------------------------------
/internal/logic/conn.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "time"
7 |
8 | "github.com/Terry-Mao/goim/api/protocol"
9 | "github.com/Terry-Mao/goim/internal/logic/model"
10 | log "github.com/golang/glog"
11 | "github.com/google/uuid"
12 | )
13 |
14 | // Connect connected a conn.
15 | func (l *Logic) Connect(c context.Context, server, cookie string, token []byte) (mid int64, key, roomID string, accepts []int32, hb int64, err error) {
16 | var params struct {
17 | Mid int64 `json:"mid"`
18 | Key string `json:"key"`
19 | RoomID string `json:"room_id"`
20 | Platform string `json:"platform"`
21 | Accepts []int32 `json:"accepts"`
22 | }
23 | if err = json.Unmarshal(token, ¶ms); err != nil {
24 | log.Errorf("json.Unmarshal(%s) error(%v)", token, err)
25 | return
26 | }
27 | mid = params.Mid
28 | roomID = params.RoomID
29 | accepts = params.Accepts
30 | hb = int64(l.c.Node.Heartbeat) * int64(l.c.Node.HeartbeatMax)
31 | if key = params.Key; key == "" {
32 | key = uuid.New().String()
33 | }
34 | if err = l.dao.AddMapping(c, mid, key, server); err != nil {
35 | log.Errorf("l.dao.AddMapping(%d,%s,%s) error(%v)", mid, key, server, err)
36 | }
37 | log.Infof("conn connected key:%s server:%s mid:%d token:%s", key, server, mid, token)
38 | return
39 | }
40 |
41 | // Disconnect disconnect a conn.
42 | func (l *Logic) Disconnect(c context.Context, mid int64, key, server string) (has bool, err error) {
43 | if has, err = l.dao.DelMapping(c, mid, key, server); err != nil {
44 | log.Errorf("l.dao.DelMapping(%d,%s) error(%v)", mid, key, server)
45 | return
46 | }
47 | log.Infof("conn disconnected key:%s server:%s mid:%d", key, server, mid)
48 | return
49 | }
50 |
51 | // Heartbeat heartbeat a conn.
52 | func (l *Logic) Heartbeat(c context.Context, mid int64, key, server string) (err error) {
53 | has, err := l.dao.ExpireMapping(c, mid, key)
54 | if err != nil {
55 | log.Errorf("l.dao.ExpireMapping(%d,%s,%s) error(%v)", mid, key, server, err)
56 | return
57 | }
58 | if !has {
59 | if err = l.dao.AddMapping(c, mid, key, server); err != nil {
60 | log.Errorf("l.dao.AddMapping(%d,%s,%s) error(%v)", mid, key, server, err)
61 | return
62 | }
63 | }
64 | log.Infof("conn heartbeat key:%s server:%s mid:%d", key, server, mid)
65 | return
66 | }
67 |
68 | // RenewOnline renew a server online.
69 | func (l *Logic) RenewOnline(c context.Context, server string, roomCount map[string]int32) (map[string]int32, error) {
70 | online := &model.Online{
71 | Server: server,
72 | RoomCount: roomCount,
73 | Updated: time.Now().Unix(),
74 | }
75 | if err := l.dao.AddServerOnline(context.Background(), server, online); err != nil {
76 | return nil, err
77 | }
78 | return l.roomCount, nil
79 | }
80 |
81 | // Receive receive a message.
82 | func (l *Logic) Receive(c context.Context, mid int64, proto *protocol.Proto) (err error) {
83 | log.Infof("receive mid:%d message:%+v", mid, proto)
84 | return
85 | }
86 |
--------------------------------------------------------------------------------
/internal/job/push.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/Terry-Mao/goim/api/comet"
8 | pb "github.com/Terry-Mao/goim/api/logic"
9 | "github.com/Terry-Mao/goim/api/protocol"
10 | "github.com/Terry-Mao/goim/pkg/bytes"
11 | log "github.com/golang/glog"
12 | )
13 |
14 | func (j *Job) push(ctx context.Context, pushMsg *pb.PushMsg) (err error) {
15 | switch pushMsg.Type {
16 | case pb.PushMsg_PUSH:
17 | err = j.pushKeys(pushMsg.Operation, pushMsg.Server, pushMsg.Keys, pushMsg.Msg)
18 | case pb.PushMsg_ROOM:
19 | err = j.getRoom(pushMsg.Room).Push(pushMsg.Operation, pushMsg.Msg)
20 | case pb.PushMsg_BROADCAST:
21 | err = j.broadcast(pushMsg.Operation, pushMsg.Msg, pushMsg.Speed)
22 | default:
23 | err = fmt.Errorf("no match push type: %s", pushMsg.Type)
24 | }
25 | return
26 | }
27 |
28 | // pushKeys push a message to a batch of subkeys.
29 | func (j *Job) pushKeys(operation int32, serverID string, subKeys []string, body []byte) (err error) {
30 | buf := bytes.NewWriterSize(len(body) + 64)
31 | p := &protocol.Proto{
32 | Ver: 1,
33 | Op: operation,
34 | Body: body,
35 | }
36 | p.WriteTo(buf)
37 | p.Body = buf.Buffer()
38 | p.Op = protocol.OpRaw
39 | var args = comet.PushMsgReq{
40 | Keys: subKeys,
41 | ProtoOp: operation,
42 | Proto: p,
43 | }
44 | if c, ok := j.cometServers[serverID]; ok {
45 | if err = c.Push(&args); err != nil {
46 | log.Errorf("c.Push(%v) serverID:%s error(%v)", args, serverID, err)
47 | }
48 | log.Infof("pushKey:%s comets:%d", serverID, len(j.cometServers))
49 | }
50 | return
51 | }
52 |
53 | // broadcast broadcast a message to all.
54 | func (j *Job) broadcast(operation int32, body []byte, speed int32) (err error) {
55 | buf := bytes.NewWriterSize(len(body) + 64)
56 | p := &protocol.Proto{
57 | Ver: 1,
58 | Op: operation,
59 | Body: body,
60 | }
61 | p.WriteTo(buf)
62 | p.Body = buf.Buffer()
63 | p.Op = protocol.OpRaw
64 | comets := j.cometServers
65 | speed /= int32(len(comets))
66 | var args = comet.BroadcastReq{
67 | ProtoOp: operation,
68 | Proto: p,
69 | Speed: speed,
70 | }
71 | for serverID, c := range comets {
72 | if err = c.Broadcast(&args); err != nil {
73 | log.Errorf("c.Broadcast(%v) serverID:%s error(%v)", args, serverID, err)
74 | }
75 | }
76 | log.Infof("broadcast comets:%d", len(comets))
77 | return
78 | }
79 |
80 | // broadcastRoomRawBytes broadcast aggregation messages to room.
81 | func (j *Job) broadcastRoomRawBytes(roomID string, body []byte) (err error) {
82 | args := comet.BroadcastRoomReq{
83 | RoomID: roomID,
84 | Proto: &protocol.Proto{
85 | Ver: 1,
86 | Op: protocol.OpRaw,
87 | Body: body,
88 | },
89 | }
90 | comets := j.cometServers
91 | for serverID, c := range comets {
92 | if err = c.BroadcastRoom(&args); err != nil {
93 | log.Errorf("c.BroadcastRoom(%v) roomID:%s serverID:%s error(%v)", args, roomID, serverID, err)
94 | }
95 | }
96 | log.Infof("broadcastRoom comets:%d", len(comets))
97 | return
98 | }
99 |
--------------------------------------------------------------------------------
/internal/comet/operation.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/Terry-Mao/goim/api/logic"
8 | "github.com/Terry-Mao/goim/api/protocol"
9 | "github.com/Terry-Mao/goim/pkg/strings"
10 | log "github.com/golang/glog"
11 |
12 | "google.golang.org/grpc"
13 | "google.golang.org/grpc/encoding/gzip"
14 | )
15 |
16 | // Connect connected a connection.
17 | func (s *Server) Connect(c context.Context, p *protocol.Proto, cookie string) (mid int64, key, rid string, accepts []int32, heartbeat time.Duration, err error) {
18 | reply, err := s.rpcClient.Connect(c, &logic.ConnectReq{
19 | Server: s.serverID,
20 | Cookie: cookie,
21 | Token: p.Body,
22 | })
23 | if err != nil {
24 | return
25 | }
26 | return reply.Mid, reply.Key, reply.RoomID, reply.Accepts, time.Duration(reply.Heartbeat), nil
27 | }
28 |
29 | // Disconnect disconnected a connection.
30 | func (s *Server) Disconnect(c context.Context, mid int64, key string) (err error) {
31 | _, err = s.rpcClient.Disconnect(context.Background(), &logic.DisconnectReq{
32 | Server: s.serverID,
33 | Mid: mid,
34 | Key: key,
35 | })
36 | return
37 | }
38 |
39 | // Heartbeat heartbeat a connection session.
40 | func (s *Server) Heartbeat(ctx context.Context, mid int64, key string) (err error) {
41 | _, err = s.rpcClient.Heartbeat(ctx, &logic.HeartbeatReq{
42 | Server: s.serverID,
43 | Mid: mid,
44 | Key: key,
45 | })
46 | return
47 | }
48 |
49 | // RenewOnline renew room online.
50 | func (s *Server) RenewOnline(ctx context.Context, serverID string, roomCount map[string]int32) (allRoom map[string]int32, err error) {
51 | reply, err := s.rpcClient.RenewOnline(ctx, &logic.OnlineReq{
52 | Server: s.serverID,
53 | RoomCount: roomCount,
54 | }, grpc.UseCompressor(gzip.Name))
55 | if err != nil {
56 | return
57 | }
58 | return reply.AllRoomCount, nil
59 | }
60 |
61 | // Receive receive a message.
62 | func (s *Server) Receive(ctx context.Context, mid int64, p *protocol.Proto) (err error) {
63 | _, err = s.rpcClient.Receive(ctx, &logic.ReceiveReq{Mid: mid, Proto: p})
64 | return
65 | }
66 |
67 | // Operate operate.
68 | func (s *Server) Operate(ctx context.Context, p *protocol.Proto, ch *Channel, b *Bucket) error {
69 | switch p.Op {
70 | case protocol.OpChangeRoom:
71 | if err := b.ChangeRoom(string(p.Body), ch); err != nil {
72 | log.Errorf("b.ChangeRoom(%s) error(%v)", p.Body, err)
73 | }
74 | p.Op = protocol.OpChangeRoomReply
75 | case protocol.OpSub:
76 | if ops, err := strings.SplitInt32s(string(p.Body), ","); err == nil {
77 | ch.Watch(ops...)
78 | }
79 | p.Op = protocol.OpSubReply
80 | case protocol.OpUnsub:
81 | if ops, err := strings.SplitInt32s(string(p.Body), ","); err == nil {
82 | ch.UnWatch(ops...)
83 | }
84 | p.Op = protocol.OpUnsubReply
85 | default:
86 | // TODO ack ok&failed
87 | if err := s.Receive(ctx, ch.Mid, p); err != nil {
88 | log.Errorf("s.Report(%d) op:%d error(%v)", ch.Mid, p.Op, err)
89 | }
90 | p.Body = nil
91 | }
92 | return nil
93 | }
94 |
--------------------------------------------------------------------------------
/internal/logic/grpc/server.go:
--------------------------------------------------------------------------------
1 | package grpc
2 |
3 | import (
4 | "context"
5 | "net"
6 | "time"
7 |
8 | pb "github.com/Terry-Mao/goim/api/logic"
9 | "github.com/Terry-Mao/goim/internal/logic"
10 | "github.com/Terry-Mao/goim/internal/logic/conf"
11 |
12 | "google.golang.org/grpc"
13 | "google.golang.org/grpc/keepalive"
14 |
15 | // use gzip decoder
16 | _ "google.golang.org/grpc/encoding/gzip"
17 | )
18 |
19 | // New logic grpc server
20 | func New(c *conf.RPCServer, l *logic.Logic) *grpc.Server {
21 | keepParams := grpc.KeepaliveParams(keepalive.ServerParameters{
22 | MaxConnectionIdle: time.Duration(c.IdleTimeout),
23 | MaxConnectionAgeGrace: time.Duration(c.ForceCloseWait),
24 | Time: time.Duration(c.KeepAliveInterval),
25 | Timeout: time.Duration(c.KeepAliveTimeout),
26 | MaxConnectionAge: time.Duration(c.MaxLifeTime),
27 | })
28 | srv := grpc.NewServer(keepParams)
29 | pb.RegisterLogicServer(srv, &server{l})
30 | lis, err := net.Listen(c.Network, c.Addr)
31 | if err != nil {
32 | panic(err)
33 | }
34 | go func() {
35 | if err := srv.Serve(lis); err != nil {
36 | panic(err)
37 | }
38 | }()
39 | return srv
40 | }
41 |
42 | type server struct {
43 | srv *logic.Logic
44 | }
45 |
46 | var _ pb.LogicServer = &server{}
47 |
48 | // Connect connect a conn.
49 | func (s *server) Connect(ctx context.Context, req *pb.ConnectReq) (*pb.ConnectReply, error) {
50 | mid, key, room, accepts, hb, err := s.srv.Connect(ctx, req.Server, req.Cookie, req.Token)
51 | if err != nil {
52 | return &pb.ConnectReply{}, err
53 | }
54 | return &pb.ConnectReply{Mid: mid, Key: key, RoomID: room, Accepts: accepts, Heartbeat: hb}, nil
55 | }
56 |
57 | // Disconnect disconnect a conn.
58 | func (s *server) Disconnect(ctx context.Context, req *pb.DisconnectReq) (*pb.DisconnectReply, error) {
59 | has, err := s.srv.Disconnect(ctx, req.Mid, req.Key, req.Server)
60 | if err != nil {
61 | return &pb.DisconnectReply{}, err
62 | }
63 | return &pb.DisconnectReply{Has: has}, nil
64 | }
65 |
66 | // Heartbeat beartbeat a conn.
67 | func (s *server) Heartbeat(ctx context.Context, req *pb.HeartbeatReq) (*pb.HeartbeatReply, error) {
68 | if err := s.srv.Heartbeat(ctx, req.Mid, req.Key, req.Server); err != nil {
69 | return &pb.HeartbeatReply{}, err
70 | }
71 | return &pb.HeartbeatReply{}, nil
72 | }
73 |
74 | // RenewOnline renew server online.
75 | func (s *server) RenewOnline(ctx context.Context, req *pb.OnlineReq) (*pb.OnlineReply, error) {
76 | allRoomCount, err := s.srv.RenewOnline(ctx, req.Server, req.RoomCount)
77 | if err != nil {
78 | return &pb.OnlineReply{}, err
79 | }
80 | return &pb.OnlineReply{AllRoomCount: allRoomCount}, nil
81 | }
82 |
83 | // Receive receive a message.
84 | func (s *server) Receive(ctx context.Context, req *pb.ReceiveReq) (*pb.ReceiveReply, error) {
85 | if err := s.srv.Receive(ctx, req.Mid, req.Proto); err != nil {
86 | return &pb.ReceiveReply{}, err
87 | }
88 | return &pb.ReceiveReply{}, nil
89 | }
90 |
91 | // nodes return nodes.
92 | func (s *server) Nodes(ctx context.Context, req *pb.NodesReq) (*pb.NodesReply, error) {
93 | return s.srv.NodesWeighted(ctx, req.Platform, req.ClientIP), nil
94 | }
95 |
--------------------------------------------------------------------------------
/benchmarks/push/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Start Command eg : ./push 0 20000 localhost:7172 60
4 |
5 | import (
6 | "bytes"
7 | "encoding/json"
8 | "fmt"
9 | "io"
10 | "io/ioutil"
11 | "log"
12 | "net"
13 | "net/http"
14 | "os"
15 | "runtime"
16 | "strconv"
17 | "time"
18 | )
19 |
20 | var (
21 | httpClient *http.Client
22 | t int
23 | )
24 |
25 | const testContent = "{\"test\":1}"
26 |
27 | type pushBodyMsg struct {
28 | Msg json.RawMessage `json:"m"`
29 | UserID int64 `json:"u"`
30 | }
31 |
32 | func init() {
33 | httpTransport := &http.Transport{
34 | Dial: func(netw, addr string) (net.Conn, error) {
35 | deadline := time.Now().Add(30 * time.Second)
36 | c, err := net.DialTimeout(netw, addr, 20*time.Second)
37 | if err != nil {
38 | return nil, err
39 | }
40 |
41 | _ = c.SetDeadline(deadline)
42 | return c, nil
43 | },
44 | DisableKeepAlives: false,
45 | }
46 | httpClient = &http.Client{
47 | Transport: httpTransport,
48 | }
49 | }
50 |
51 | func main() {
52 | runtime.GOMAXPROCS(runtime.NumCPU())
53 | begin, err := strconv.Atoi(os.Args[1])
54 | if err != nil {
55 | panic(err)
56 | }
57 | length, err := strconv.Atoi(os.Args[2])
58 | if err != nil {
59 | panic(err)
60 | }
61 |
62 | t, err = strconv.Atoi(os.Args[4])
63 | if err != nil {
64 | panic(err)
65 | }
66 |
67 | num := runtime.NumCPU() * 2
68 | log.Printf("start routine num:%d", num)
69 |
70 | l := length / num
71 | b, e := begin, begin+l
72 | time.AfterFunc(time.Duration(t)*time.Second, stop)
73 | for i := 0; i < num; i++ {
74 | go startPush(b, e)
75 | b += l
76 | e += l
77 | }
78 | if b < begin+length {
79 | go startPush(b, begin+length)
80 | }
81 |
82 | time.Sleep(9999 * time.Hour)
83 | }
84 |
85 | func stop() {
86 | os.Exit(-1)
87 | }
88 |
89 | func startPush(b, e int) {
90 | log.Printf("start Push from %d to %d", b, e)
91 | bodys := make([][]byte, e-b)
92 | for i := 0; i < e-b; i++ {
93 | msg := &pushBodyMsg{Msg: json.RawMessage(testContent), UserID: int64(b)}
94 | body, err := json.Marshal(msg)
95 | if err != nil {
96 | panic(err)
97 | }
98 | bodys[i] = body
99 | }
100 |
101 | for {
102 | for i := 0; i < len(bodys); i++ {
103 | resp, err := httpPost(fmt.Sprintf("http://%s/goim/push/mids?operation=1000&mids=%d", os.Args[3], b), "application/x-www-form-urlencoded", bytes.NewBuffer(bodys[i]))
104 | if err != nil {
105 | log.Printf("post error (%v)", err)
106 | continue
107 | }
108 |
109 | body, err := ioutil.ReadAll(resp.Body)
110 | if err != nil {
111 | log.Printf("post error (%v)", err)
112 | return
113 | }
114 | resp.Body.Close()
115 |
116 | log.Printf("response %s", string(body))
117 | //time.Sleep(50 * time.Millisecond)
118 | }
119 | }
120 | }
121 |
122 | func httpPost(url string, contentType string, body io.Reader) (*http.Response, error) {
123 | req, err := http.NewRequest("POST", url, body)
124 | if err != nil {
125 | return nil, err
126 | }
127 |
128 | req.Header.Set("Content-Type", contentType)
129 | resp, err := httpClient.Do(req)
130 | if err != nil {
131 | return nil, err
132 | }
133 |
134 | return resp, nil
135 | }
136 |
--------------------------------------------------------------------------------
/benchmarks/multi_push/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Start Command eg : ./multi_push 0 20000 localhost:7172 60
4 |
5 | import (
6 | "bytes"
7 | "encoding/json"
8 | "fmt"
9 | "io"
10 | "io/ioutil"
11 | "log"
12 | "net"
13 | "net/http"
14 | "os"
15 | "runtime"
16 | "strconv"
17 | "time"
18 | )
19 |
20 | var (
21 | lg *log.Logger
22 | httpClient *http.Client
23 | t int
24 | )
25 |
26 | const testContent = "{\"test\":1}"
27 |
28 | type pushsBodyMsg struct {
29 | Msg json.RawMessage `json:"m"`
30 | UserIds []int64 `json:"u"`
31 | }
32 |
33 | func init() {
34 | httpTransport := &http.Transport{
35 | Dial: func(netw, addr string) (net.Conn, error) {
36 | deadline := time.Now().Add(30 * time.Second)
37 | c, err := net.DialTimeout(netw, addr, 20*time.Second)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | _ = c.SetDeadline(deadline)
43 | return c, nil
44 | },
45 | DisableKeepAlives: false,
46 | }
47 | httpClient = &http.Client{
48 | Transport: httpTransport,
49 | }
50 | }
51 |
52 | func main() {
53 | runtime.GOMAXPROCS(runtime.NumCPU())
54 | infoLogfi, err := os.OpenFile("./multi_push.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
55 | if err != nil {
56 | panic(err)
57 | }
58 | lg = log.New(infoLogfi, "", log.LstdFlags|log.Lshortfile)
59 |
60 | begin, err := strconv.Atoi(os.Args[1])
61 | if err != nil {
62 | panic(err)
63 | }
64 | length, err := strconv.Atoi(os.Args[2])
65 | if err != nil {
66 | panic(err)
67 | }
68 |
69 | t, err = strconv.Atoi(os.Args[4])
70 | if err != nil {
71 | panic(err)
72 | }
73 |
74 | num := runtime.NumCPU() * 8
75 |
76 | l := length / num
77 | b, e := begin, begin+l
78 | time.AfterFunc(time.Duration(t)*time.Second, stop)
79 | for i := 0; i < num; i++ {
80 | go startPush(b, e)
81 | b += l
82 | e += l
83 | }
84 | if b < begin+length {
85 | go startPush(b, begin+length)
86 | }
87 |
88 | time.Sleep(9999 * time.Hour)
89 | }
90 |
91 | func stop() {
92 | os.Exit(-1)
93 | }
94 |
95 | func startPush(b, e int) {
96 | l := make([]int64, 0, e-b)
97 | for i := b; i < e; i++ {
98 | l = append(l, int64(i))
99 | }
100 | msg := &pushsBodyMsg{Msg: json.RawMessage(testContent), UserIds: l}
101 | body, err := json.Marshal(msg)
102 | if err != nil {
103 | panic(err)
104 | }
105 | for {
106 | resp, err := httpPost(fmt.Sprintf("http://%s/goim/push/mids=%d", os.Args[3], b), "application/x-www-form-urlencoded", bytes.NewBuffer(body))
107 | if err != nil {
108 | lg.Printf("post error (%v)", err)
109 | continue
110 | }
111 |
112 | body, err := ioutil.ReadAll(resp.Body)
113 | if err != nil {
114 | lg.Printf("post error (%v)", err)
115 | return
116 | }
117 | resp.Body.Close()
118 |
119 | lg.Printf("response %s", string(body))
120 | }
121 | }
122 |
123 | func httpPost(url string, contentType string, body io.Reader) (*http.Response, error) {
124 | req, err := http.NewRequest("POST", url, body)
125 | if err != nil {
126 | return nil, err
127 | }
128 |
129 | req.Header.Set("Content-Type", contentType)
130 | resp, err := httpClient.Do(req)
131 | if err != nil {
132 | return nil, err
133 | }
134 |
135 | return resp, nil
136 | }
137 |
--------------------------------------------------------------------------------
/README_en.md:
--------------------------------------------------------------------------------
1 | goim
2 | ==============
3 | `Terry-Mao/goim` is a IM and push notification server cluster.
4 |
5 | ---------------------------------------
6 | * [Features](#features)
7 | * [Installing](#installing)
8 | * [Configurations](#configurations)
9 | * [Examples](#examples)
10 | * [Documents](#documents)
11 | * [More](#more)
12 |
13 | ---------------------------------------
14 |
15 | ## Features
16 | * Light weight
17 | * High performance
18 | * Pure Golang
19 | * Supports single push, multiple push, room push and broadcasting
20 | * Supports one key to multiple subscribers (Configurable maximum subscribers count)
21 | * Supports heartbeats (Application heartbeats, TCP, KeepAlive)
22 | * Supports authentication (Unauthenticated user can't subscribe)
23 | * Supports multiple protocols (WebSocket,TCP)
24 | * Scalable architecture (Unlimited dynamic job and logic modules)
25 | * Asynchronous push notification based on Kafka
26 |
27 | ## Installing
28 | ### Dependencies
29 | ```sh
30 | $ yum -y install java-1.7.0-openjdk
31 | ```
32 |
33 | ### Install Kafka
34 |
35 | Please follow the official quick start [here](http://kafka.apache.org/documentation.html#quickstart).
36 |
37 | ### Install Golang environment
38 |
39 | Please follow the official quick start [here](https://golang.org/doc/install).
40 |
41 | ### Deploy goim
42 | 1.Download goim
43 | ```sh
44 | $ yum install git
45 | $ cd $GOPATH/src
46 | $ git clone https://github.com/Terry-Mao/goim.git
47 | $ cd $GOPATH/src/goim
48 | $ go get ./...
49 | ```
50 |
51 | 2.Install router、logic、comet、job modules(You might need to change the configuration files based on your servers)
52 | ```sh
53 | $ cd $GOPATH/src/goim/router
54 | $ go install
55 | $ cp router-example.conf $GOPATH/bin/router.conf
56 | $ cp router-log.xml $GOPATH/bin/
57 | $ cd ../logic/
58 | $ go install
59 | $ cp logic-example.conf $GOPATH/bin/logic.conf
60 | $ cp logic-log.xml $GOPATH/bin/
61 | $ cd ../comet/
62 | $ go install
63 | $ cp comet-example.conf $GOPATH/bin/comet.conf
64 | $ cp comet-log.xml $GOPATH/bin/
65 | $ cd ../logic/job/
66 | $ go install
67 | $ cp job-example.conf $GOPATH/bin/job.conf
68 | $ cp job-log.xml $GOPATH/bin/
69 | ```
70 |
71 | Everything is DONE!
72 |
73 | ### Run goim
74 | You may need to change the log files location.
75 | ```sh
76 | $ cd /$GOPATH/bin
77 | $ nohup $GOPATH/bin/router -c $GOPATH/bin/router.conf 2>&1 > /data/logs/goim/panic-router.log &
78 | $ nohup $GOPATH/bin/logic -c $GOPATH/bin/logic.conf 2>&1 > /data/logs/goim/panic-logic.log &
79 | $ nohup $GOPATH/bin/comet -c $GOPATH/bin/comet.conf 2>&1 > /data/logs/goim/panic-comet.log &
80 | $ nohup $GOPATH/bin/job -c $GOPATH/bin/job.conf 2>&1 > /data/logs/goim/panic-job.log &
81 | ```
82 |
83 | If it fails, please check the logs for debugging.
84 |
85 | ### Testing
86 |
87 | Check the push protocols here[push HTTP protocols](./docs/push.md)
88 |
89 | ## Configurations
90 | TODO
91 |
92 | ## Examples
93 | Websocket: [Websocket Client Demo](https://github.com/Terry-Mao/goim/tree/master/examples/javascript)
94 |
95 | Android: [Android SDK](https://github.com/roamdy/goim-sdk)
96 |
97 | iOS: [iOS](https://github.com/roamdy/goim-oc-sdk)
98 |
99 | ## Documents
100 | [push HTTP protocols](./docs/en/push.md)
101 |
102 | [Comet client protocols](./docs/en/proto.md)
103 |
104 | ##More
105 | TODO
106 |
--------------------------------------------------------------------------------
/internal/comet/grpc/server.go:
--------------------------------------------------------------------------------
1 | package grpc
2 |
3 | import (
4 | "context"
5 | "net"
6 | "time"
7 |
8 | pb "github.com/Terry-Mao/goim/api/comet"
9 | "github.com/Terry-Mao/goim/internal/comet"
10 | "github.com/Terry-Mao/goim/internal/comet/conf"
11 | "github.com/Terry-Mao/goim/internal/comet/errors"
12 |
13 | "google.golang.org/grpc"
14 | "google.golang.org/grpc/keepalive"
15 | )
16 |
17 | // New comet grpc server.
18 | func New(c *conf.RPCServer, s *comet.Server) *grpc.Server {
19 | keepParams := grpc.KeepaliveParams(keepalive.ServerParameters{
20 | MaxConnectionIdle: time.Duration(c.IdleTimeout),
21 | MaxConnectionAgeGrace: time.Duration(c.ForceCloseWait),
22 | Time: time.Duration(c.KeepAliveInterval),
23 | Timeout: time.Duration(c.KeepAliveTimeout),
24 | MaxConnectionAge: time.Duration(c.MaxLifeTime),
25 | })
26 | srv := grpc.NewServer(keepParams)
27 | pb.RegisterCometServer(srv, &server{s})
28 | lis, err := net.Listen(c.Network, c.Addr)
29 | if err != nil {
30 | panic(err)
31 | }
32 | go func() {
33 | if err := srv.Serve(lis); err != nil {
34 | panic(err)
35 | }
36 | }()
37 | return srv
38 | }
39 |
40 | type server struct {
41 | srv *comet.Server
42 | }
43 |
44 | var _ pb.CometServer = &server{}
45 |
46 | // PushMsg push a message to specified sub keys.
47 | func (s *server) PushMsg(ctx context.Context, req *pb.PushMsgReq) (reply *pb.PushMsgReply, err error) {
48 | if len(req.Keys) == 0 || req.Proto == nil {
49 | return nil, errors.ErrPushMsgArg
50 | }
51 | for _, key := range req.Keys {
52 | bucket := s.srv.Bucket(key)
53 | if bucket == nil {
54 | continue
55 | }
56 | if channel := bucket.Channel(key); channel != nil {
57 | if !channel.NeedPush(req.ProtoOp) {
58 | continue
59 | }
60 | if err = channel.Push(req.Proto); err != nil {
61 | return
62 | }
63 | }
64 | }
65 | return &pb.PushMsgReply{}, nil
66 | }
67 |
68 | // Broadcast broadcast msg to all user.
69 | func (s *server) Broadcast(ctx context.Context, req *pb.BroadcastReq) (*pb.BroadcastReply, error) {
70 | if req.Proto == nil {
71 | return nil, errors.ErrBroadCastArg
72 | }
73 | // TODO use broadcast queue
74 | go func() {
75 | for _, bucket := range s.srv.Buckets() {
76 | bucket.Broadcast(req.GetProto(), req.ProtoOp)
77 | if req.Speed > 0 {
78 | t := bucket.ChannelCount() / int(req.Speed)
79 | time.Sleep(time.Duration(t) * time.Second)
80 | }
81 | }
82 | }()
83 | return &pb.BroadcastReply{}, nil
84 | }
85 |
86 | // BroadcastRoom broadcast msg to specified room.
87 | func (s *server) BroadcastRoom(ctx context.Context, req *pb.BroadcastRoomReq) (*pb.BroadcastRoomReply, error) {
88 | if req.Proto == nil || req.RoomID == "" {
89 | return nil, errors.ErrBroadCastRoomArg
90 | }
91 | for _, bucket := range s.srv.Buckets() {
92 | bucket.BroadcastRoom(req)
93 | }
94 | return &pb.BroadcastRoomReply{}, nil
95 | }
96 |
97 | // Rooms gets all the room ids for the server.
98 | func (s *server) Rooms(ctx context.Context, req *pb.RoomsReq) (*pb.RoomsReply, error) {
99 | var (
100 | roomIds = make(map[string]bool)
101 | )
102 | for _, bucket := range s.srv.Buckets() {
103 | for roomID := range bucket.Rooms() {
104 | roomIds[roomID] = true
105 | }
106 | }
107 | return &pb.RoomsReply{Rooms: roomIds}, nil
108 | }
109 |
--------------------------------------------------------------------------------
/internal/job/room.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/Terry-Mao/goim/api/protocol"
8 | "github.com/Terry-Mao/goim/internal/job/conf"
9 | "github.com/Terry-Mao/goim/pkg/bytes"
10 | log "github.com/golang/glog"
11 | )
12 |
13 | var (
14 | // ErrComet commet error.
15 | ErrComet = errors.New("comet rpc is not available")
16 | // ErrCometFull comet chan full.
17 | ErrCometFull = errors.New("comet proto chan full")
18 | // ErrRoomFull room chan full.
19 | ErrRoomFull = errors.New("room proto chan full")
20 |
21 | roomReadyProto = new(protocol.Proto)
22 | )
23 |
24 | // Room room.
25 | type Room struct {
26 | c *conf.Room
27 | job *Job
28 | id string
29 | proto chan *protocol.Proto
30 | }
31 |
32 | // NewRoom new a room struct, store channel room info.
33 | func NewRoom(job *Job, id string, c *conf.Room) (r *Room) {
34 | r = &Room{
35 | c: c,
36 | id: id,
37 | job: job,
38 | proto: make(chan *protocol.Proto, c.Batch*2),
39 | }
40 | go r.pushproc(c.Batch, time.Duration(c.Signal))
41 | return
42 | }
43 |
44 | // Push push msg to the room, if chan full discard it.
45 | func (r *Room) Push(op int32, msg []byte) (err error) {
46 | var p = &protocol.Proto{
47 | Ver: 1,
48 | Op: op,
49 | Body: msg,
50 | }
51 | select {
52 | case r.proto <- p:
53 | default:
54 | err = ErrRoomFull
55 | }
56 | return
57 | }
58 |
59 | // pushproc merge proto and push msgs in batch.
60 | func (r *Room) pushproc(batch int, sigTime time.Duration) {
61 | var (
62 | n int
63 | last time.Time
64 | p *protocol.Proto
65 | buf = bytes.NewWriterSize(int(protocol.MaxBodySize))
66 | )
67 | log.Infof("start room:%s goroutine", r.id)
68 | td := time.AfterFunc(sigTime, func() {
69 | select {
70 | case r.proto <- roomReadyProto:
71 | default:
72 | }
73 | })
74 | defer td.Stop()
75 | for {
76 | if p = <-r.proto; p == nil {
77 | break // exit
78 | } else if p != roomReadyProto {
79 | // merge buffer ignore error, always nil
80 | p.WriteTo(buf)
81 | if n++; n == 1 {
82 | last = time.Now()
83 | td.Reset(sigTime)
84 | continue
85 | } else if n < batch {
86 | if sigTime > time.Since(last) {
87 | continue
88 | }
89 | }
90 | } else {
91 | if n == 0 {
92 | break
93 | }
94 | }
95 | _ = r.job.broadcastRoomRawBytes(r.id, buf.Buffer())
96 | // TODO use reset buffer
97 | // after push to room channel, renew a buffer, let old buffer gc
98 | buf = bytes.NewWriterSize(buf.Size())
99 | n = 0
100 | if r.c.Idle != 0 {
101 | td.Reset(time.Duration(r.c.Idle))
102 | } else {
103 | td.Reset(time.Minute)
104 | }
105 | }
106 | r.job.delRoom(r.id)
107 | log.Infof("room:%s goroutine exit", r.id)
108 | }
109 |
110 | func (j *Job) delRoom(roomID string) {
111 | j.roomsMutex.Lock()
112 | delete(j.rooms, roomID)
113 | j.roomsMutex.Unlock()
114 | }
115 |
116 | func (j *Job) getRoom(roomID string) *Room {
117 | j.roomsMutex.RLock()
118 | room, ok := j.rooms[roomID]
119 | j.roomsMutex.RUnlock()
120 | if !ok {
121 | j.roomsMutex.Lock()
122 | if room, ok = j.rooms[roomID]; !ok {
123 | room = NewRoom(j, roomID, j.c.Room)
124 | j.rooms[roomID] = room
125 | }
126 | j.roomsMutex.Unlock()
127 | log.Infof("new a room:%s active:%d", roomID, len(j.rooms))
128 | }
129 | return room
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | goim v2.0
2 | ==============
3 |
4 | [](https://golang.org/)
5 | [](https://github.com/Terry-Mao/goim/actions)
6 | [](https://pkg.go.dev/github.com/Terry-Mao/goim)
7 | [](https://goreportcard.com/report/github.com/Terry-Mao/goim)
8 |
9 | goim is an im server writen in golang.
10 |
11 | ## Features
12 | * Light weight
13 | * High performance
14 | * Pure Golang
15 | * Supports single push, multiple push and broadcasting
16 | * Supports one key to multiple subscribers (Configurable maximum subscribers count)
17 | * Supports heartbeats (Application heartbeats, TCP, KeepAlive, HTTP long pulling)
18 | * Supports authentication (Unauthenticated user can't subscribe)
19 | * Supports multiple protocols (WebSocket,TCP,HTTP)
20 | * Scalable architecture (Unlimited dynamic job and logic modules)
21 | * Asynchronous push notification based on Kafka
22 |
23 | ## Architecture
24 | 
25 |
26 | ## Quick Start
27 |
28 | ### Build
29 | ```
30 | make build
31 | ```
32 |
33 | ### Run
34 | ```
35 | make run
36 | make stop
37 |
38 | // or
39 | nohup target/logic -conf=target/logic.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 2>&1 > target/logic.log &
40 | nohup target/comet -conf=target/comet.toml -region=sh -zone=sh001 -deploy.env=dev -weight=10 -addrs=127.0.0.1 2>&1 > target/logic.log &
41 | nohup target/job -conf=target/job.toml -region=sh -zone=sh001 -deploy.env=dev 2>&1 > target/logic.log &
42 |
43 | ```
44 | ### Environment
45 | ```
46 | env:
47 | export REGION=sh
48 | export ZONE=sh001
49 | export DEPLOY_ENV=dev
50 |
51 | supervisor:
52 | environment=REGION=sh,ZONE=sh001,DEPLOY_ENV=dev
53 |
54 | go flag:
55 | -region=sh -zone=sh001 deploy.env=dev
56 | ```
57 | ### Configuration
58 | You can view the comments in target/comet.toml,logic.toml,job.toml to understand the meaning of the config.
59 |
60 | ### Dependencies
61 | [Discovery](https://github.com/bilibili/discovery)
62 |
63 | [Kafka](https://kafka.apache.org/quickstart)
64 |
65 | ## Document
66 | [Protocol](./docs/protocol.png)
67 |
68 | [English](./README_en.md)
69 |
70 | [中文](./README_cn.md)
71 |
72 | ## Examples
73 | Websocket: [Websocket Client Demo](https://github.com/Terry-Mao/goim/tree/master/examples/javascript)
74 |
75 | Android: [Android](https://github.com/roamdy/goim-sdk)
76 |
77 | iOS: [iOS](https://github.com/roamdy/goim-oc-sdk)
78 |
79 | ## Benchmark
80 | 
81 |
82 | ### Benchmark Server
83 | | CPU | Memory | OS | Instance |
84 | | :---- | :---- | :---- | :---- |
85 | | Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz | DDR3 32GB | Debian GNU/Linux 8 | 1 |
86 |
87 | ### Benchmark Case
88 | * Online: 1,000,000
89 | * Duration: 15min
90 | * Push Speed: 40/s (broadcast room)
91 | * Push Message: {"test":1}
92 | * Received calc mode: 1s per times, total 30 times
93 |
94 | ### Benchmark Resource
95 | * CPU: 2000%~2300%
96 | * Memory: 14GB
97 | * GC Pause: 504ms
98 | * Network: Incoming(450MBit/s), Outgoing(4.39GBit/s)
99 |
100 | ### Benchmark Result
101 | * Received: 35,900,000/s
102 |
103 | [中文](./docs/benchmark_cn.md)
104 |
105 | [English](./docs/benchmark_en.md)
106 |
107 | ## LICENSE
108 | goim is is distributed under the terms of the MIT License.
109 |
--------------------------------------------------------------------------------
/README_cn.md:
--------------------------------------------------------------------------------
1 | goim v2.0
2 | ==============
3 | `Terry-Mao/goim` 是一个支持集群的im及实时推送服务。
4 |
5 | ---------------------------------------
6 | * [特性](#特性)
7 | * [安装](#安装)
8 | * [配置](#配置)
9 | * [例子](#例子)
10 | * [文档](#文档)
11 | * [集群](#集群)
12 | * [更多](#更多)
13 |
14 | ---------------------------------------
15 |
16 | ## 特性
17 | * 轻量级
18 | * 高性能
19 | * 纯Golang实现
20 | * 支持单个、多个、单房间以及广播消息推送
21 | * 支持单个Key多个订阅者(可限制订阅者最大人数)
22 | * 心跳支持(应用心跳和tcp、keepalive)
23 | * 支持安全验证(未授权用户不能订阅)
24 | * 多协议支持(websocket,tcp)
25 | * 可拓扑的架构(job、logic模块可动态无限扩展)
26 | * 基于Kafka做异步消息推送
27 |
28 | ## 安装
29 | ### 一、安装依赖
30 | ```sh
31 | $ yum -y install java-1.7.0-openjdk
32 | ```
33 |
34 | ### 二、安装Kafka消息队列服务
35 |
36 | kafka在官网已经描述的非常详细,在这里就不过多说明,安装、启动请查看[这里](http://kafka.apache.org/documentation.html#quickstart).
37 |
38 | ### 三、搭建golang环境
39 | 1.下载源码(根据自己的系统下载对应的[安装包](http://golang.org/dl/))
40 | ```sh
41 | $ cd /data/programfiles
42 | $ wget -c --no-check-certificate https://storage.googleapis.com/golang/go1.5.2.linux-amd64.tar.gz
43 | $ tar -xvf go1.5.2.linux-amd64.tar.gz -C /usr/local
44 | ```
45 | 2.配置GO环境变量
46 | (这里我加在/etc/profile.d/golang.sh)
47 | ```sh
48 | $ vi /etc/profile.d/golang.sh
49 | # 将以下环境变量添加到profile最后面
50 | export GOROOT=/usr/local/go
51 | export PATH=$PATH:$GOROOT/bin
52 | export GOPATH=/data/apps/go
53 | $ source /etc/profile
54 | ```
55 |
56 | ### 四、部署goim
57 | 1.下载goim及依赖包
58 | ```sh
59 | $ yum install hg
60 | $ go get -u github.com/Terry-Mao/goim
61 | $ mv $GOPATH/src/github.com/Terry-Mao/goim $GOPATH/src/goim
62 | $ cd $GOPATH/src/goim
63 | $ go get ./...
64 | ```
65 |
66 | 2.安装router、logic、comet、job模块(配置文件请依据实际机器环境配置)
67 | ```sh
68 | $ cd $GOPATH/src/goim/router
69 | $ go install
70 | $ cp router-example.conf $GOPATH/bin/router.conf
71 | $ cp router-log.xml $GOPATH/bin/
72 | $ cd ../logic/
73 | $ go install
74 | $ cp logic-example.conf $GOPATH/bin/logic.conf
75 | $ cp logic-log.xml $GOPATH/bin/
76 | $ cd ../comet/
77 | $ go install
78 | $ cp comet-example.conf $GOPATH/bin/comet.conf
79 | $ cp comet-log.xml $GOPATH/bin/
80 | $ cd ../logic/job/
81 | $ go install
82 | $ cp job-example.conf $GOPATH/bin/job.conf
83 | $ cp job-log.xml $GOPATH/bin/
84 | ```
85 | 到此所有的环境都搭建完成!
86 |
87 | ### 五、启动goim
88 | ```sh
89 | $ cd /$GOPATH/bin
90 | $ nohup $GOPATH/bin/router -c $GOPATH/bin/router.conf 2>&1 > /data/logs/goim/panic-router.log &
91 | $ nohup $GOPATH/bin/logic -c $GOPATH/bin/logic.conf 2>&1 > /data/logs/goim/panic-logic.log &
92 | $ nohup $GOPATH/bin/comet -c $GOPATH/bin/comet.conf 2>&1 > /data/logs/goim/panic-comet.log &
93 | $ nohup $GOPATH/bin/job -c $GOPATH/bin/job.conf 2>&1 > /data/logs/goim/panic-job.log &
94 | ```
95 | 如果启动失败,默认配置可通过查看panic-xxx.log日志文件来排查各个模块问题.
96 |
97 | ### 六、测试
98 |
99 | 推送协议可查看[push http协议文档](./docs/push.md)
100 |
101 | ## 配置
102 |
103 | TODO
104 |
105 | ## 例子
106 |
107 | Websocket: [Websocket Client Demo](https://github.com/Terry-Mao/goim/tree/master/examples/javascript)
108 |
109 | Android: [Android](https://github.com/roamdy/goim-sdk)
110 |
111 | iOS: [iOS](https://github.com/roamdy/goim-oc-sdk)
112 |
113 | ## 文档
114 | [push http协议文档](./docs/push.md)推送接口
115 |
116 | ## 集群
117 |
118 | ### comet
119 |
120 | comet 属于接入层,非常容易扩展,直接开启多个comet节点,修改配置文件中的base节点下的server.id修改成不同值(注意一定要保证不同的comet进程值唯一),前端接入可以使用LVS 或者 DNS来转发
121 |
122 | ### logic
123 |
124 | logic 属于无状态的逻辑层,可以随意增加节点,使用nginx upstream来扩展http接口,内部rpc部分,可以使用LVS四层转发
125 |
126 | ### kafka
127 |
128 | kafka 可以使用多broker,或者多partition来扩展队列
129 |
130 | ### router
131 |
132 | router 属于有状态节点,logic可以使用一致性hash配置节点,增加多个router节点(目前还不支持动态扩容),提前预估好在线和压力情况
133 |
134 | ### job
135 |
136 | job 根据kafka的partition来扩展多job工作方式,具体可以参考下kafka的partition负载
137 |
138 | ## 更多
139 | TODO
140 |
--------------------------------------------------------------------------------
/cmd/comet/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "math/rand"
8 | "net"
9 | "os"
10 | "os/signal"
11 | "runtime"
12 | "strconv"
13 | "strings"
14 | "syscall"
15 | "time"
16 |
17 | "github.com/bilibili/discovery/naming"
18 | resolver "github.com/bilibili/discovery/naming/grpc"
19 | "github.com/Terry-Mao/goim/internal/comet"
20 | "github.com/Terry-Mao/goim/internal/comet/conf"
21 | "github.com/Terry-Mao/goim/internal/comet/grpc"
22 | md "github.com/Terry-Mao/goim/internal/logic/model"
23 | "github.com/Terry-Mao/goim/pkg/ip"
24 | log "github.com/golang/glog"
25 | )
26 |
27 | const (
28 | ver = "2.0.0"
29 | appid = "goim.comet"
30 | )
31 |
32 | func main() {
33 | flag.Parse()
34 | if err := conf.Init(); err != nil {
35 | panic(err)
36 | }
37 | rand.Seed(time.Now().UTC().UnixNano())
38 | runtime.GOMAXPROCS(runtime.NumCPU())
39 | println(conf.Conf.Debug)
40 | log.Infof("goim-comet [version: %s env: %+v] start", ver, conf.Conf.Env)
41 | // register discovery
42 | dis := naming.New(conf.Conf.Discovery)
43 | resolver.Register(dis)
44 | // new comet server
45 | srv := comet.NewServer(conf.Conf)
46 | if err := comet.InitWhitelist(conf.Conf.Whitelist); err != nil {
47 | panic(err)
48 | }
49 | if err := comet.InitTCP(srv, conf.Conf.TCP.Bind, runtime.NumCPU()); err != nil {
50 | panic(err)
51 | }
52 | if err := comet.InitWebsocket(srv, conf.Conf.Websocket.Bind, runtime.NumCPU()); err != nil {
53 | panic(err)
54 | }
55 | if conf.Conf.Websocket.TLSOpen {
56 | if err := comet.InitWebsocketWithTLS(srv, conf.Conf.Websocket.TLSBind, conf.Conf.Websocket.CertFile, conf.Conf.Websocket.PrivateFile, runtime.NumCPU()); err != nil {
57 | panic(err)
58 | }
59 | }
60 | // new grpc server
61 | rpcSrv := grpc.New(conf.Conf.RPCServer, srv)
62 | cancel := register(dis, srv)
63 | // signal
64 | c := make(chan os.Signal, 1)
65 | signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
66 | for {
67 | s := <-c
68 | log.Infof("goim-comet get a signal %s", s.String())
69 | switch s {
70 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
71 | if cancel != nil {
72 | cancel()
73 | }
74 | rpcSrv.GracefulStop()
75 | srv.Close()
76 | log.Infof("goim-comet [version: %s] exit", ver)
77 | log.Flush()
78 | return
79 | case syscall.SIGHUP:
80 | default:
81 | return
82 | }
83 | }
84 | }
85 |
86 | func register(dis *naming.Discovery, srv *comet.Server) context.CancelFunc {
87 | env := conf.Conf.Env
88 | addr := ip.InternalIP()
89 | _, port, _ := net.SplitHostPort(conf.Conf.RPCServer.Addr)
90 | ins := &naming.Instance{
91 | Region: env.Region,
92 | Zone: env.Zone,
93 | Env: env.DeployEnv,
94 | Hostname: env.Host,
95 | AppID: appid,
96 | Addrs: []string{
97 | "grpc://" + addr + ":" + port,
98 | },
99 | Metadata: map[string]string{
100 | md.MetaWeight: strconv.FormatInt(env.Weight, 10),
101 | md.MetaOffline: strconv.FormatBool(env.Offline),
102 | md.MetaAddrs: strings.Join(env.Addrs, ","),
103 | },
104 | }
105 | cancel, err := dis.Register(ins)
106 | if err != nil {
107 | panic(err)
108 | }
109 | // renew discovery metadata
110 | go func() {
111 | for {
112 | var (
113 | err error
114 | conns int
115 | ips = make(map[string]struct{})
116 | )
117 | for _, bucket := range srv.Buckets() {
118 | for ip := range bucket.IPCount() {
119 | ips[ip] = struct{}{}
120 | }
121 | conns += bucket.ChannelCount()
122 | }
123 | ins.Metadata[md.MetaConnCount] = fmt.Sprint(conns)
124 | ins.Metadata[md.MetaIPCount] = fmt.Sprint(len(ips))
125 | if err = dis.Set(ins); err != nil {
126 | log.Errorf("dis.Set(%+v) error(%v)", ins, err)
127 | time.Sleep(time.Second)
128 | continue
129 | }
130 | time.Sleep(time.Second * 10)
131 | }
132 | }()
133 | return cancel
134 | }
135 |
--------------------------------------------------------------------------------
/api/protocol/protocol.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // source: protocol/protocol.proto
3 |
4 | package protocol
5 |
6 | import (
7 | fmt "fmt"
8 | proto "github.com/golang/protobuf/proto"
9 | math "math"
10 | )
11 |
12 | // Reference imports to suppress errors if they are not otherwise used.
13 | var _ = proto.Marshal
14 | var _ = fmt.Errorf
15 | var _ = math.Inf
16 |
17 | // This is a compile-time assertion to ensure that this generated file
18 | // is compatible with the proto package it is being compiled against.
19 | // A compilation error at this line likely means your copy of the
20 | // proto package needs to be updated.
21 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
22 |
23 | //
24 | // v1.0.0
25 | // protocol
26 | type Proto struct {
27 | Ver int32 `protobuf:"varint,1,opt,name=ver,proto3" json:"ver,omitempty"`
28 | Op int32 `protobuf:"varint,2,opt,name=op,proto3" json:"op,omitempty"`
29 | Seq int32 `protobuf:"varint,3,opt,name=seq,proto3" json:"seq,omitempty"`
30 | Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"`
31 | XXX_NoUnkeyedLiteral struct{} `json:"-"`
32 | XXX_unrecognized []byte `json:"-"`
33 | XXX_sizecache int32 `json:"-"`
34 | }
35 |
36 | func (m *Proto) Reset() { *m = Proto{} }
37 | func (m *Proto) String() string { return proto.CompactTextString(m) }
38 | func (*Proto) ProtoMessage() {}
39 | func (*Proto) Descriptor() ([]byte, []int) {
40 | return fileDescriptor_87968d26f3046c60, []int{0}
41 | }
42 |
43 | func (m *Proto) XXX_Unmarshal(b []byte) error {
44 | return xxx_messageInfo_Proto.Unmarshal(m, b)
45 | }
46 | func (m *Proto) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
47 | return xxx_messageInfo_Proto.Marshal(b, m, deterministic)
48 | }
49 | func (m *Proto) XXX_Merge(src proto.Message) {
50 | xxx_messageInfo_Proto.Merge(m, src)
51 | }
52 | func (m *Proto) XXX_Size() int {
53 | return xxx_messageInfo_Proto.Size(m)
54 | }
55 | func (m *Proto) XXX_DiscardUnknown() {
56 | xxx_messageInfo_Proto.DiscardUnknown(m)
57 | }
58 |
59 | var xxx_messageInfo_Proto proto.InternalMessageInfo
60 |
61 | func (m *Proto) GetVer() int32 {
62 | if m != nil {
63 | return m.Ver
64 | }
65 | return 0
66 | }
67 |
68 | func (m *Proto) GetOp() int32 {
69 | if m != nil {
70 | return m.Op
71 | }
72 | return 0
73 | }
74 |
75 | func (m *Proto) GetSeq() int32 {
76 | if m != nil {
77 | return m.Seq
78 | }
79 | return 0
80 | }
81 |
82 | func (m *Proto) GetBody() []byte {
83 | if m != nil {
84 | return m.Body
85 | }
86 | return nil
87 | }
88 |
89 | func init() {
90 | proto.RegisterType((*Proto)(nil), "goim.protocol.Proto")
91 | }
92 |
93 | func init() { proto.RegisterFile("protocol/protocol.proto", fileDescriptor_87968d26f3046c60) }
94 |
95 | var fileDescriptor_87968d26f3046c60 = []byte{
96 | // 153 bytes of a gzipped FileDescriptorProto
97 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2f, 0x28, 0xca, 0x2f,
98 | 0xc9, 0x4f, 0xce, 0xcf, 0xd1, 0x87, 0x31, 0xf4, 0xc0, 0x0c, 0x21, 0xde, 0xf4, 0xfc, 0xcc, 0x5c,
99 | 0x3d, 0x98, 0xa0, 0x92, 0x3f, 0x17, 0x6b, 0x00, 0x58, 0x5c, 0x80, 0x8b, 0xb9, 0x2c, 0xb5, 0x48,
100 | 0x82, 0x51, 0x81, 0x51, 0x83, 0x35, 0x08, 0xc4, 0x14, 0xe2, 0xe3, 0x62, 0xca, 0x2f, 0x90, 0x60,
101 | 0x02, 0x0b, 0x30, 0xe5, 0x17, 0x80, 0x54, 0x14, 0xa7, 0x16, 0x4a, 0x30, 0x43, 0x54, 0x14, 0xa7,
102 | 0x16, 0x0a, 0x09, 0x71, 0xb1, 0x24, 0xe5, 0xa7, 0x54, 0x4a, 0xb0, 0x28, 0x30, 0x6a, 0xf0, 0x04,
103 | 0x81, 0xd9, 0x4e, 0x86, 0x51, 0xfa, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9,
104 | 0xfa, 0x21, 0xa9, 0x45, 0x45, 0x95, 0xba, 0xbe, 0x89, 0xf9, 0xfa, 0x20, 0x6b, 0xf5, 0x13, 0x0b,
105 | 0x32, 0xe1, 0xee, 0xb1, 0x86, 0x31, 0x92, 0xd8, 0xc0, 0x2c, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff,
106 | 0xff, 0x8d, 0xc7, 0xbf, 0x64, 0xb4, 0x00, 0x00, 0x00,
107 | }
108 |
--------------------------------------------------------------------------------
/internal/comet/server.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "context"
5 | "math/rand"
6 | "time"
7 |
8 | "github.com/Terry-Mao/goim/api/logic"
9 | "github.com/Terry-Mao/goim/internal/comet/conf"
10 | log "github.com/golang/glog"
11 | "github.com/zhenjl/cityhash"
12 | "google.golang.org/grpc"
13 | "google.golang.org/grpc/balancer/roundrobin"
14 | "google.golang.org/grpc/keepalive"
15 | )
16 |
17 | const (
18 | minServerHeartbeat = time.Minute * 10
19 | maxServerHeartbeat = time.Minute * 30
20 | // grpc options
21 | grpcInitialWindowSize = 1 << 24
22 | grpcInitialConnWindowSize = 1 << 24
23 | grpcMaxSendMsgSize = 1 << 24
24 | grpcMaxCallMsgSize = 1 << 24
25 | grpcKeepAliveTime = time.Second * 10
26 | grpcKeepAliveTimeout = time.Second * 3
27 | grpcBackoffMaxDelay = time.Second * 3
28 | )
29 |
30 | func newLogicClient(c *conf.RPCClient) logic.LogicClient {
31 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.Dial))
32 | defer cancel()
33 | conn, err := grpc.DialContext(ctx, "discovery://default/goim.logic",
34 | []grpc.DialOption{
35 | grpc.WithInsecure(),
36 | grpc.WithInitialWindowSize(grpcInitialWindowSize),
37 | grpc.WithInitialConnWindowSize(grpcInitialConnWindowSize),
38 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(grpcMaxCallMsgSize)),
39 | grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(grpcMaxSendMsgSize)),
40 | grpc.WithBackoffMaxDelay(grpcBackoffMaxDelay),
41 | grpc.WithKeepaliveParams(keepalive.ClientParameters{
42 | Time: grpcKeepAliveTime,
43 | Timeout: grpcKeepAliveTimeout,
44 | PermitWithoutStream: true,
45 | }),
46 | grpc.WithBalancerName(roundrobin.Name),
47 | }...)
48 | if err != nil {
49 | panic(err)
50 | }
51 | return logic.NewLogicClient(conn)
52 | }
53 |
54 | // Server is comet server.
55 | type Server struct {
56 | c *conf.Config
57 | round *Round // accept round store
58 | buckets []*Bucket // subkey bucket
59 | bucketIdx uint32
60 |
61 | serverID string
62 | rpcClient logic.LogicClient
63 | }
64 |
65 | // NewServer returns a new Server.
66 | func NewServer(c *conf.Config) *Server {
67 | s := &Server{
68 | c: c,
69 | round: NewRound(c),
70 | rpcClient: newLogicClient(c.RPCClient),
71 | }
72 | // init bucket
73 | s.buckets = make([]*Bucket, c.Bucket.Size)
74 | s.bucketIdx = uint32(c.Bucket.Size)
75 | for i := 0; i < c.Bucket.Size; i++ {
76 | s.buckets[i] = NewBucket(c.Bucket)
77 | }
78 | s.serverID = c.Env.Host
79 | go s.onlineproc()
80 | return s
81 | }
82 |
83 | // Buckets return all buckets.
84 | func (s *Server) Buckets() []*Bucket {
85 | return s.buckets
86 | }
87 |
88 | // Bucket get the bucket by subkey.
89 | func (s *Server) Bucket(subKey string) *Bucket {
90 | idx := cityhash.CityHash32([]byte(subKey), uint32(len(subKey))) % s.bucketIdx
91 | if conf.Conf.Debug {
92 | log.Infof("%s hit channel bucket index: %d use cityhash", subKey, idx)
93 | }
94 | return s.buckets[idx]
95 | }
96 |
97 | // RandServerHearbeat rand server heartbeat.
98 | func (s *Server) RandServerHearbeat() time.Duration {
99 | return (minServerHeartbeat + time.Duration(rand.Int63n(int64(maxServerHeartbeat-minServerHeartbeat))))
100 | }
101 |
102 | // Close close the server.
103 | func (s *Server) Close() (err error) {
104 | return
105 | }
106 |
107 | func (s *Server) onlineproc() {
108 | for {
109 | var (
110 | allRoomsCount map[string]int32
111 | err error
112 | )
113 | roomCount := make(map[string]int32)
114 | for _, bucket := range s.buckets {
115 | for roomID, count := range bucket.RoomsCount() {
116 | roomCount[roomID] += count
117 | }
118 | }
119 | if allRoomsCount, err = s.RenewOnline(context.Background(), s.serverID, roomCount); err != nil {
120 | time.Sleep(time.Second)
121 | continue
122 | }
123 | for _, bucket := range s.buckets {
124 | bucket.UpRoomsCount(allRoomsCount)
125 | }
126 | time.Sleep(time.Second * 10)
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/internal/job/job.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sync"
7 | "time"
8 |
9 | pb "github.com/Terry-Mao/goim/api/logic"
10 | "github.com/Terry-Mao/goim/internal/job/conf"
11 | "github.com/bilibili/discovery/naming"
12 | "github.com/golang/protobuf/proto"
13 |
14 | cluster "github.com/bsm/sarama-cluster"
15 | log "github.com/golang/glog"
16 | )
17 |
18 | // Job is push job.
19 | type Job struct {
20 | c *conf.Config
21 | consumer *cluster.Consumer
22 | cometServers map[string]*Comet
23 |
24 | rooms map[string]*Room
25 | roomsMutex sync.RWMutex
26 | }
27 |
28 | // New new a push job.
29 | func New(c *conf.Config) *Job {
30 | j := &Job{
31 | c: c,
32 | consumer: newKafkaSub(c.Kafka),
33 | rooms: make(map[string]*Room),
34 | }
35 | j.watchComet(c.Discovery)
36 | return j
37 | }
38 |
39 | func newKafkaSub(c *conf.Kafka) *cluster.Consumer {
40 | config := cluster.NewConfig()
41 | config.Consumer.Return.Errors = true
42 | config.Group.Return.Notifications = true
43 | consumer, err := cluster.NewConsumer(c.Brokers, c.Group, []string{c.Topic}, config)
44 | if err != nil {
45 | panic(err)
46 | }
47 | return consumer
48 | }
49 |
50 | // Close close resounces.
51 | func (j *Job) Close() error {
52 | if j.consumer != nil {
53 | return j.consumer.Close()
54 | }
55 | return nil
56 | }
57 |
58 | // Consume messages, watch signals
59 | func (j *Job) Consume() {
60 | for {
61 | select {
62 | case err := <-j.consumer.Errors():
63 | log.Errorf("consumer error(%v)", err)
64 | case n := <-j.consumer.Notifications():
65 | log.Infof("consumer rebalanced(%v)", n)
66 | case msg, ok := <-j.consumer.Messages():
67 | if !ok {
68 | return
69 | }
70 | j.consumer.MarkOffset(msg, "")
71 | // process push message
72 | pushMsg := new(pb.PushMsg)
73 | if err := proto.Unmarshal(msg.Value, pushMsg); err != nil {
74 | log.Errorf("proto.Unmarshal(%v) error(%v)", msg, err)
75 | continue
76 | }
77 | if err := j.push(context.Background(), pushMsg); err != nil {
78 | log.Errorf("j.push(%v) error(%v)", pushMsg, err)
79 | }
80 | log.Infof("consume: %s/%d/%d\t%s\t%+v", msg.Topic, msg.Partition, msg.Offset, msg.Key, pushMsg)
81 | }
82 | }
83 | }
84 |
85 | func (j *Job) watchComet(c *naming.Config) {
86 | dis := naming.New(c)
87 | resolver := dis.Build("goim.comet")
88 | event := resolver.Watch()
89 | select {
90 | case _, ok := <-event:
91 | if !ok {
92 | panic("watchComet init failed")
93 | }
94 | if ins, ok := resolver.Fetch(); ok {
95 | if err := j.newAddress(ins.Instances); err != nil {
96 | panic(err)
97 | }
98 | log.Infof("watchComet init newAddress:%+v", ins)
99 | }
100 | case <-time.After(10 * time.Second):
101 | log.Error("watchComet init instances timeout")
102 | }
103 | go func() {
104 | for {
105 | if _, ok := <-event; !ok {
106 | log.Info("watchComet exit")
107 | return
108 | }
109 | ins, ok := resolver.Fetch()
110 | if ok {
111 | if err := j.newAddress(ins.Instances); err != nil {
112 | log.Errorf("watchComet newAddress(%+v) error(%+v)", ins, err)
113 | continue
114 | }
115 | log.Infof("watchComet change newAddress:%+v", ins)
116 | }
117 | }
118 | }()
119 | }
120 |
121 | func (j *Job) newAddress(insMap map[string][]*naming.Instance) error {
122 | ins := insMap[j.c.Env.Zone]
123 | if len(ins) == 0 {
124 | return fmt.Errorf("watchComet instance is empty")
125 | }
126 | comets := map[string]*Comet{}
127 | for _, in := range ins {
128 | if old, ok := j.cometServers[in.Hostname]; ok {
129 | comets[in.Hostname] = old
130 | continue
131 | }
132 | c, err := NewComet(in, j.c.Comet)
133 | if err != nil {
134 | log.Errorf("watchComet NewComet(%+v) error(%v)", in, err)
135 | return err
136 | }
137 | comets[in.Hostname] = c
138 | log.Infof("watchComet AddComet grpc:%+v", in)
139 | }
140 | for key, old := range j.cometServers {
141 | if _, ok := comets[key]; !ok {
142 | old.cancel()
143 | log.Infof("watchComet DelComet:%s", key)
144 | }
145 | }
146 | j.cometServers = comets
147 | return nil
148 | }
149 |
--------------------------------------------------------------------------------
/internal/logic/logic.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "context"
5 | "strconv"
6 | "time"
7 |
8 | "github.com/Terry-Mao/goim/internal/logic/conf"
9 | "github.com/Terry-Mao/goim/internal/logic/dao"
10 | "github.com/Terry-Mao/goim/internal/logic/model"
11 | "github.com/bilibili/discovery/naming"
12 | log "github.com/golang/glog"
13 | )
14 |
15 | const (
16 | _onlineTick = time.Second * 10
17 | _onlineDeadline = time.Minute * 5
18 | )
19 |
20 | // Logic struct
21 | type Logic struct {
22 | c *conf.Config
23 | dis *naming.Discovery
24 | dao *dao.Dao
25 | // online
26 | totalIPs int64
27 | totalConns int64
28 | roomCount map[string]int32
29 | // load balancer
30 | nodes []*naming.Instance
31 | loadBalancer *LoadBalancer
32 | regions map[string]string // province -> region
33 | }
34 |
35 | // New init
36 | func New(c *conf.Config) (l *Logic) {
37 | l = &Logic{
38 | c: c,
39 | dao: dao.New(c),
40 | dis: naming.New(c.Discovery),
41 | loadBalancer: NewLoadBalancer(),
42 | regions: make(map[string]string),
43 | }
44 | l.initRegions()
45 | l.initNodes()
46 | _ = l.loadOnline()
47 | go l.onlineproc()
48 | return l
49 | }
50 |
51 | // Ping ping resources is ok.
52 | func (l *Logic) Ping(c context.Context) (err error) {
53 | return l.dao.Ping(c)
54 | }
55 |
56 | // Close close resources.
57 | func (l *Logic) Close() {
58 | l.dao.Close()
59 | }
60 |
61 | func (l *Logic) initRegions() {
62 | for region, ps := range l.c.Regions {
63 | for _, province := range ps {
64 | l.regions[province] = region
65 | }
66 | }
67 | }
68 |
69 | func (l *Logic) initNodes() {
70 | res := l.dis.Build("goim.comet")
71 | event := res.Watch()
72 | select {
73 | case _, ok := <-event:
74 | if ok {
75 | l.newNodes(res)
76 | } else {
77 | panic("discovery watch failed")
78 | }
79 | case <-time.After(10 * time.Second):
80 | log.Error("discovery start timeout")
81 | }
82 | go func() {
83 | for {
84 | if _, ok := <-event; !ok {
85 | return
86 | }
87 | l.newNodes(res)
88 | }
89 | }()
90 | }
91 |
92 | func (l *Logic) newNodes(res naming.Resolver) {
93 | if zoneIns, ok := res.Fetch(); ok {
94 | var (
95 | totalConns int64
96 | totalIPs int64
97 | allIns []*naming.Instance
98 | )
99 | for _, zins := range zoneIns.Instances {
100 | for _, ins := range zins {
101 | if ins.Metadata == nil {
102 | log.Errorf("node instance metadata is empty(%+v)", ins)
103 | continue
104 | }
105 | offline, err := strconv.ParseBool(ins.Metadata[model.MetaOffline])
106 | if err != nil || offline {
107 | log.Warningf("strconv.ParseBool(offline:%t) error(%v)", offline, err)
108 | continue
109 | }
110 | conns, err := strconv.ParseInt(ins.Metadata[model.MetaConnCount], 10, 32)
111 | if err != nil {
112 | log.Errorf("strconv.ParseInt(conns:%d) error(%v)", conns, err)
113 | continue
114 | }
115 | ips, err := strconv.ParseInt(ins.Metadata[model.MetaIPCount], 10, 32)
116 | if err != nil {
117 | log.Errorf("strconv.ParseInt(ips:%d) error(%v)", ips, err)
118 | continue
119 | }
120 | totalConns += conns
121 | totalIPs += ips
122 | allIns = append(allIns, ins)
123 | }
124 | }
125 | l.totalConns = totalConns
126 | l.totalIPs = totalIPs
127 | l.nodes = allIns
128 | l.loadBalancer.Update(allIns)
129 | }
130 | }
131 |
132 | func (l *Logic) onlineproc() {
133 | for {
134 | time.Sleep(_onlineTick)
135 | if err := l.loadOnline(); err != nil {
136 | log.Errorf("onlineproc error(%v)", err)
137 | }
138 | }
139 | }
140 |
141 | func (l *Logic) loadOnline() (err error) {
142 | var (
143 | roomCount = make(map[string]int32)
144 | )
145 | for _, server := range l.nodes {
146 | var online *model.Online
147 | online, err = l.dao.ServerOnline(context.Background(), server.Hostname)
148 | if err != nil {
149 | return
150 | }
151 | if time.Since(time.Unix(online.Updated, 0)) > _onlineDeadline {
152 | _ = l.dao.DelServerOnline(context.Background(), server.Hostname)
153 | continue
154 | }
155 | for roomID, count := range online.RoomCount {
156 | roomCount[roomID] += count
157 | }
158 | }
159 | l.roomCount = roomCount
160 | return
161 | }
162 |
--------------------------------------------------------------------------------
/internal/logic/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/bilibili/discovery/naming"
10 | xtime "github.com/Terry-Mao/goim/pkg/time"
11 |
12 | "github.com/BurntSushi/toml"
13 | )
14 |
15 | var (
16 | confPath string
17 | region string
18 | zone string
19 | deployEnv string
20 | host string
21 | weight int64
22 |
23 | // Conf config
24 | Conf *Config
25 | )
26 |
27 | func init() {
28 | var (
29 | defHost, _ = os.Hostname()
30 | defWeight, _ = strconv.ParseInt(os.Getenv("WEIGHT"), 10, 32)
31 | )
32 | flag.StringVar(&confPath, "conf", "logic-example.toml", "default config path")
33 | flag.StringVar(®ion, "region", os.Getenv("REGION"), "avaliable region. or use REGION env variable, value: sh etc.")
34 | flag.StringVar(&zone, "zone", os.Getenv("ZONE"), "avaliable zone. or use ZONE env variable, value: sh001/sh002 etc.")
35 | flag.StringVar(&deployEnv, "deploy.env", os.Getenv("DEPLOY_ENV"), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.")
36 | flag.StringVar(&host, "host", defHost, "machine hostname. or use default machine hostname.")
37 | flag.Int64Var(&weight, "weight", defWeight, "load balancing weight, or use WEIGHT env variable, value: 10 etc.")
38 | }
39 |
40 | // Init init config.
41 | func Init() (err error) {
42 | Conf = Default()
43 | _, err = toml.DecodeFile(confPath, &Conf)
44 | return
45 | }
46 |
47 | // Default new a config with specified defualt value.
48 | func Default() *Config {
49 | return &Config{
50 | Env: &Env{Region: region, Zone: zone, DeployEnv: deployEnv, Host: host, Weight: weight},
51 | Discovery: &naming.Config{Region: region, Zone: zone, Env: deployEnv, Host: host},
52 | HTTPServer: &HTTPServer{
53 | Network: "tcp",
54 | Addr: "3111",
55 | ReadTimeout: xtime.Duration(time.Second),
56 | WriteTimeout: xtime.Duration(time.Second),
57 | },
58 | RPCClient: &RPCClient{Dial: xtime.Duration(time.Second), Timeout: xtime.Duration(time.Second)},
59 | RPCServer: &RPCServer{
60 | Network: "tcp",
61 | Addr: "3119",
62 | Timeout: xtime.Duration(time.Second),
63 | IdleTimeout: xtime.Duration(time.Second * 60),
64 | MaxLifeTime: xtime.Duration(time.Hour * 2),
65 | ForceCloseWait: xtime.Duration(time.Second * 20),
66 | KeepAliveInterval: xtime.Duration(time.Second * 60),
67 | KeepAliveTimeout: xtime.Duration(time.Second * 20),
68 | },
69 | Backoff: &Backoff{MaxDelay: 300, BaseDelay: 3, Factor: 1.8, Jitter: 1.3},
70 | }
71 | }
72 |
73 | // Config config.
74 | type Config struct {
75 | Env *Env
76 | Discovery *naming.Config
77 | RPCClient *RPCClient
78 | RPCServer *RPCServer
79 | HTTPServer *HTTPServer
80 | Kafka *Kafka
81 | Redis *Redis
82 | Node *Node
83 | Backoff *Backoff
84 | Regions map[string][]string
85 | }
86 |
87 | // Env is env config.
88 | type Env struct {
89 | Region string
90 | Zone string
91 | DeployEnv string
92 | Host string
93 | Weight int64
94 | }
95 |
96 | // Node node config.
97 | type Node struct {
98 | DefaultDomain string
99 | HostDomain string
100 | TCPPort int
101 | WSPort int
102 | WSSPort int
103 | HeartbeatMax int
104 | Heartbeat xtime.Duration
105 | RegionWeight float64
106 | }
107 |
108 | // Backoff backoff.
109 | type Backoff struct {
110 | MaxDelay int32
111 | BaseDelay int32
112 | Factor float32
113 | Jitter float32
114 | }
115 |
116 | // Redis .
117 | type Redis struct {
118 | Network string
119 | Addr string
120 | Auth string
121 | Active int
122 | Idle int
123 | DialTimeout xtime.Duration
124 | ReadTimeout xtime.Duration
125 | WriteTimeout xtime.Duration
126 | IdleTimeout xtime.Duration
127 | Expire xtime.Duration
128 | }
129 |
130 | // Kafka .
131 | type Kafka struct {
132 | Topic string
133 | Brokers []string
134 | }
135 |
136 | // RPCClient is RPC client config.
137 | type RPCClient struct {
138 | Dial xtime.Duration
139 | Timeout xtime.Duration
140 | }
141 |
142 | // RPCServer is RPC server config.
143 | type RPCServer struct {
144 | Network string
145 | Addr string
146 | Timeout xtime.Duration
147 | IdleTimeout xtime.Duration
148 | MaxLifeTime xtime.Duration
149 | ForceCloseWait xtime.Duration
150 | KeepAliveInterval xtime.Duration
151 | KeepAliveTimeout xtime.Duration
152 | }
153 |
154 | // HTTPServer is http server config.
155 | type HTTPServer struct {
156 | Network string
157 | Addr string
158 | ReadTimeout xtime.Duration
159 | WriteTimeout xtime.Duration
160 | }
161 |
--------------------------------------------------------------------------------
/docs/push.md:
--------------------------------------------------------------------------------
1 | ## goim push API
2 |
3 | ### error codes
4 | ```
5 | // ok
6 | OK = 0
7 |
8 | // request error
9 | RequestErr = -400
10 |
11 | // server error
12 | ServerErr = -500
13 | ```
14 |
15 | ### push keys
16 | [POST] /goim/push/keys
17 |
18 | | Name | Type | Remork |
19 | |:----------------|:--------:|:-----------------------|
20 | | [url]:operation | int32 | operation for response |
21 | | [url]:keys | []string | multiple client keys |
22 | | [Body] | []byte | http request body |
23 |
24 | response:
25 | ```
26 | {
27 | "code": 0
28 | }
29 | ```
30 |
31 | ### push mids
32 | [POST] /goim/push/mids
33 |
34 | | Name | Type | Remork |
35 | |:----------------|:--------:|:-----------------------|
36 | | [url]:operation | int32 | operation for response |
37 | | [url]:mids | []int64 | multiple user mids |
38 | | [Body] | []byte | http request body |
39 |
40 | response:
41 | ```
42 | {
43 | "code": 0
44 | }
45 | ```
46 |
47 | ### push room
48 | [POST] /goim/push/room
49 |
50 | | Name | Type | Remork |
51 | |:----------------|:--------:|:-----------------------|
52 | | [url]:operation | int32 | operation for response |
53 | | [url]:type | string | room type |
54 | | [url]:room | string | room id |
55 | | [Body] | []byte | http request body |
56 |
57 | response:
58 | ```
59 | {
60 | "code": 0
61 | }
62 | ```
63 |
64 | ### push all
65 | [POST] /goim/push/all
66 |
67 | | Name | Type | Remork |
68 | |:----------------|:--------:|:-----------------------|
69 | | [url]:operation | int32 | operation for response |
70 | | [url]:speed | int32 | push speed |
71 | | [Body] | []byte | http request body |
72 |
73 | response:
74 | ```
75 | {
76 | "code": 0
77 | }
78 | ```
79 |
80 | ### online top
81 | [GET] /goim/online/top
82 |
83 | | Name | Type | Remork |
84 | |:--------|:--------:|:-----------------------|
85 | | type | string | room type |
86 | | limit | string | online limit |
87 |
88 | response:
89 | ```
90 | {
91 | "code": 0,
92 | "message": "",
93 | "data": [
94 | {
95 | "room_id": "1000",
96 | "count": 100
97 | },
98 | {
99 | "room_id": "2000",
100 | "count": 200
101 | },
102 | {
103 | "room_id": "3000",
104 | "count": 300
105 | }
106 | ]
107 | }
108 | ```
109 |
110 | ### online room
111 | [GET] /goim/online/room
112 |
113 | | Name | Type | Remork |
114 | |:--------|:--------:|:-----------------------|
115 | | type | string | room type |
116 | | rooms | []string | room ids |
117 |
118 | response:
119 | ```
120 | {
121 | "code": 0,
122 | "message": "",
123 | "data": {
124 | "1000": 100,
125 | "2000": 200,
126 | "3000": 300
127 | }
128 | }
129 | ```
130 | ### online total
131 | [GET] /goim/online/total
132 |
133 | response:
134 | ```
135 | {
136 | "code": 0,
137 | "message": "",
138 | "data": {
139 | "conn_count": 1,
140 | "ip_count": 1
141 | }
142 | }
143 | ```
144 |
145 | ### nodes weighted
146 | [GET] /goim/nodes/weighted
147 |
148 | | Name | Type | Remork |
149 | |:---------|:--------:|:-----------------------|
150 | | platform | string | web/android/ios |
151 |
152 | response:
153 | ```
154 | {
155 | "code": 0,
156 | "message": "",
157 | "data": {
158 | "domain": "conn.goim.io",
159 | "tcp_port": 3101,
160 | "ws_port": 3102,
161 | "wss_port": 3103,
162 | "heartbeat": 30, // heartbeat seconds
163 | "heartbeat_max": 3 // heartbeat tries
164 | "nodes": [
165 | "47.89.10.97"
166 | ],
167 | "backoff": {
168 | "max_delay": 300,
169 | "base_delay": 3,
170 | "factor": 1.8,
171 | "jitter": 0.3
172 | },
173 |
174 | }
175 | }
176 | ```
177 |
178 | ### nodes instances
179 | [GET] /nodes/instances
180 |
181 | response:
182 | ```
183 | {
184 | "code": 0,
185 | "message": "",
186 | "data": [
187 | {
188 | "region": "sh",
189 | "zone": "sh001",
190 | "env": "dev",
191 | "appid": "goim.comet",
192 | "hostname": "test",
193 | "addrs": [
194 | "grpc://192.168.1.30:3109"
195 | ],
196 | "version": "",
197 | "latest_timestamp": 1545750122311688676,
198 | "metadata": {
199 | "addrs": "47.89.10.97",
200 | "conn_count": "1",
201 | "ip_count": "1",
202 | "offline": "false",
203 | "weight": "10"
204 | }
205 | }
206 | ]
207 | }
208 | `
209 |
--------------------------------------------------------------------------------
/internal/logic/balancer.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "sort"
7 | "strconv"
8 | "strings"
9 | "sync"
10 |
11 | "github.com/bilibili/discovery/naming"
12 | "github.com/Terry-Mao/goim/internal/logic/model"
13 | log "github.com/golang/glog"
14 | )
15 |
16 | const (
17 | _minWeight = 1
18 | _maxWeight = 1 << 20
19 | _maxNodes = 5
20 | )
21 |
22 | type weightedNode struct {
23 | region string
24 | hostname string
25 | addrs []string
26 | fixedWeight int64
27 | currentWeight int64
28 | currentConns int64
29 | updated int64
30 | }
31 |
32 | func (w *weightedNode) String() string {
33 | return fmt.Sprintf("region:%s fixedWeight:%d, currentWeight:%d, currentConns:%d", w.region, w.fixedWeight, w.currentWeight, w.currentConns)
34 | }
35 |
36 | func (w *weightedNode) chosen() {
37 | w.currentConns++
38 | }
39 |
40 | func (w *weightedNode) reset() {
41 | w.currentWeight = 0
42 | }
43 |
44 | func (w *weightedNode) calculateWeight(totalWeight, totalConns int64, gainWeight float64) {
45 | fixedWeight := float64(w.fixedWeight) * gainWeight
46 | totalWeight += int64(fixedWeight) - w.fixedWeight
47 | if totalConns > 0 {
48 | weightRatio := fixedWeight / float64(totalWeight)
49 | var connRatio float64
50 | if totalConns != 0 {
51 | connRatio = float64(w.currentConns) / float64(totalConns) * 0.5
52 | }
53 | diff := weightRatio - connRatio
54 | multiple := diff * float64(totalConns)
55 | floor := math.Floor(multiple)
56 | if floor-multiple >= -0.5 {
57 | w.currentWeight = int64(fixedWeight + floor)
58 | } else {
59 | w.currentWeight = int64(fixedWeight + math.Ceil(multiple))
60 | }
61 | if diff < 0 {
62 | // we always return the max from minWeight and calculated Current weight
63 | if _minWeight > w.currentWeight {
64 | w.currentWeight = _minWeight
65 | }
66 | } else {
67 | // we always return the min from maxWeight and calculated Current weight
68 | if _maxWeight < w.currentWeight {
69 | w.currentWeight = _maxWeight
70 | }
71 | }
72 | } else {
73 | w.reset()
74 | }
75 | }
76 |
77 | // LoadBalancer load balancer.
78 | type LoadBalancer struct {
79 | totalConns int64
80 | totalWeight int64
81 | nodes map[string]*weightedNode
82 | nodesMutex sync.Mutex
83 | }
84 |
85 | // NewLoadBalancer new a load balancer.
86 | func NewLoadBalancer() *LoadBalancer {
87 | lb := &LoadBalancer{
88 | nodes: make(map[string]*weightedNode),
89 | }
90 | return lb
91 | }
92 |
93 | // Size return node size.
94 | func (lb *LoadBalancer) Size() int {
95 | return len(lb.nodes)
96 | }
97 |
98 | func (lb *LoadBalancer) weightedNodes(region string, regionWeight float64) (nodes []*weightedNode) {
99 | for _, n := range lb.nodes {
100 | var gainWeight = float64(1.0)
101 | if n.region == region {
102 | gainWeight *= regionWeight
103 | }
104 | n.calculateWeight(lb.totalWeight, lb.totalConns, gainWeight)
105 | nodes = append(nodes, n)
106 | }
107 | sort.Slice(nodes, func(i, j int) bool {
108 | return nodes[i].currentWeight > nodes[j].currentWeight
109 | })
110 | if len(nodes) > 0 {
111 | nodes[0].chosen()
112 | lb.totalConns++
113 | }
114 | return
115 | }
116 |
117 | // NodeAddrs return node addrs.
118 | func (lb *LoadBalancer) NodeAddrs(region, domain string, regionWeight float64) (domains, addrs []string) {
119 | lb.nodesMutex.Lock()
120 | nodes := lb.weightedNodes(region, regionWeight)
121 | lb.nodesMutex.Unlock()
122 | for i, n := range nodes {
123 | if i == _maxNodes {
124 | break
125 | }
126 | domains = append(domains, n.hostname+domain)
127 | addrs = append(addrs, n.addrs...)
128 | }
129 | return
130 | }
131 |
132 | // Update update server nodes.
133 | func (lb *LoadBalancer) Update(ins []*naming.Instance) {
134 | var (
135 | totalConns int64
136 | totalWeight int64
137 | nodes = make(map[string]*weightedNode, len(ins))
138 | )
139 | if len(ins) == 0 || float32(len(ins))/float32(len(lb.nodes)) < 0.5 {
140 | log.Errorf("load balancer update src:%d target:%d less than half", len(lb.nodes), len(ins))
141 | return
142 | }
143 | lb.nodesMutex.Lock()
144 | for _, in := range ins {
145 | if old, ok := lb.nodes[in.Hostname]; ok && old.updated == in.LastTs {
146 | nodes[in.Hostname] = old
147 | totalConns += old.currentConns
148 | totalWeight += old.fixedWeight
149 | } else {
150 | meta := in.Metadata
151 | weight, err := strconv.ParseInt(meta[model.MetaWeight], 10, 32)
152 | if err != nil {
153 | log.Errorf("instance(%+v) strconv.ParseInt(weight:%s) error(%v)", in, meta[model.MetaWeight], err)
154 | continue
155 | }
156 | conns, err := strconv.ParseInt(meta[model.MetaConnCount], 10, 32)
157 | if err != nil {
158 | log.Errorf("instance(%+v) strconv.ParseInt(conns:%s) error(%v)", in, meta[model.MetaConnCount], err)
159 | continue
160 | }
161 | nodes[in.Hostname] = &weightedNode{
162 | region: in.Region,
163 | hostname: in.Hostname,
164 | fixedWeight: weight,
165 | currentConns: conns,
166 | addrs: strings.Split(meta[model.MetaAddrs], ","),
167 | updated: in.LastTs,
168 | }
169 | totalConns += conns
170 | totalWeight += weight
171 | }
172 | }
173 | lb.nodes = nodes
174 | lb.totalConns = totalConns
175 | lb.totalWeight = totalWeight
176 | lb.nodesMutex.Unlock()
177 | }
178 |
--------------------------------------------------------------------------------
/internal/comet/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | "github.com/bilibili/discovery/naming"
11 | "github.com/BurntSushi/toml"
12 | xtime "github.com/Terry-Mao/goim/pkg/time"
13 | )
14 |
15 | var (
16 | confPath string
17 | region string
18 | zone string
19 | deployEnv string
20 | host string
21 | addrs string
22 | weight int64
23 | offline bool
24 | debug bool
25 |
26 | // Conf config
27 | Conf *Config
28 | )
29 |
30 | func init() {
31 | var (
32 | defHost, _ = os.Hostname()
33 | defAddrs = os.Getenv("ADDRS")
34 | defWeight, _ = strconv.ParseInt(os.Getenv("WEIGHT"), 10, 32)
35 | defOffline, _ = strconv.ParseBool(os.Getenv("OFFLINE"))
36 | defDebug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
37 | )
38 | flag.StringVar(&confPath, "conf", "comet-example.toml", "default config path.")
39 | flag.StringVar(®ion, "region", os.Getenv("REGION"), "avaliable region. or use REGION env variable, value: sh etc.")
40 | flag.StringVar(&zone, "zone", os.Getenv("ZONE"), "avaliable zone. or use ZONE env variable, value: sh001/sh002 etc.")
41 | flag.StringVar(&deployEnv, "deploy.env", os.Getenv("DEPLOY_ENV"), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.")
42 | flag.StringVar(&host, "host", defHost, "machine hostname. or use default machine hostname.")
43 | flag.StringVar(&addrs, "addrs", defAddrs, "server public ip addrs. or use ADDRS env variable, value: 127.0.0.1 etc.")
44 | flag.Int64Var(&weight, "weight", defWeight, "load balancing weight, or use WEIGHT env variable, value: 10 etc.")
45 | flag.BoolVar(&offline, "offline", defOffline, "server offline. or use OFFLINE env variable, value: true/false etc.")
46 | flag.BoolVar(&debug, "debug", defDebug, "server debug. or use DEBUG env variable, value: true/false etc.")
47 | }
48 |
49 | // Init init config.
50 | func Init() (err error) {
51 | Conf = Default()
52 | _, err = toml.DecodeFile(confPath, &Conf)
53 | return
54 | }
55 |
56 | // Default new a config with specified defualt value.
57 | func Default() *Config {
58 | return &Config{
59 | Debug: debug,
60 | Env: &Env{Region: region, Zone: zone, DeployEnv: deployEnv, Host: host, Weight: weight, Addrs: strings.Split(addrs, ","), Offline: offline},
61 | Discovery: &naming.Config{Region: region, Zone: zone, Env: deployEnv, Host: host},
62 | RPCClient: &RPCClient{
63 | Dial: xtime.Duration(time.Second),
64 | Timeout: xtime.Duration(time.Second),
65 | },
66 | RPCServer: &RPCServer{
67 | Network: "tcp",
68 | Addr: ":3109",
69 | Timeout: xtime.Duration(time.Second),
70 | IdleTimeout: xtime.Duration(time.Second * 60),
71 | MaxLifeTime: xtime.Duration(time.Hour * 2),
72 | ForceCloseWait: xtime.Duration(time.Second * 20),
73 | KeepAliveInterval: xtime.Duration(time.Second * 60),
74 | KeepAliveTimeout: xtime.Duration(time.Second * 20),
75 | },
76 | TCP: &TCP{
77 | Bind: []string{":3101"},
78 | Sndbuf: 4096,
79 | Rcvbuf: 4096,
80 | KeepAlive: false,
81 | Reader: 32,
82 | ReadBuf: 1024,
83 | ReadBufSize: 8192,
84 | Writer: 32,
85 | WriteBuf: 1024,
86 | WriteBufSize: 8192,
87 | },
88 | Websocket: &Websocket{
89 | Bind: []string{":3102"},
90 | },
91 | Protocol: &Protocol{
92 | Timer: 32,
93 | TimerSize: 2048,
94 | CliProto: 5,
95 | SvrProto: 10,
96 | HandshakeTimeout: xtime.Duration(time.Second * 5),
97 | },
98 | Bucket: &Bucket{
99 | Size: 32,
100 | Channel: 1024,
101 | Room: 1024,
102 | RoutineAmount: 32,
103 | RoutineSize: 1024,
104 | },
105 | }
106 | }
107 |
108 | // Config is comet config.
109 | type Config struct {
110 | Debug bool
111 | Env *Env
112 | Discovery *naming.Config
113 | TCP *TCP
114 | Websocket *Websocket
115 | Protocol *Protocol
116 | Bucket *Bucket
117 | RPCClient *RPCClient
118 | RPCServer *RPCServer
119 | Whitelist *Whitelist
120 | }
121 |
122 | // Env is env config.
123 | type Env struct {
124 | Region string
125 | Zone string
126 | DeployEnv string
127 | Host string
128 | Weight int64
129 | Offline bool
130 | Addrs []string
131 | }
132 |
133 | // RPCClient is RPC client config.
134 | type RPCClient struct {
135 | Dial xtime.Duration
136 | Timeout xtime.Duration
137 | }
138 |
139 | // RPCServer is RPC server config.
140 | type RPCServer struct {
141 | Network string
142 | Addr string
143 | Timeout xtime.Duration
144 | IdleTimeout xtime.Duration
145 | MaxLifeTime xtime.Duration
146 | ForceCloseWait xtime.Duration
147 | KeepAliveInterval xtime.Duration
148 | KeepAliveTimeout xtime.Duration
149 | }
150 |
151 | // TCP is tcp config.
152 | type TCP struct {
153 | Bind []string
154 | Sndbuf int
155 | Rcvbuf int
156 | KeepAlive bool
157 | Reader int
158 | ReadBuf int
159 | ReadBufSize int
160 | Writer int
161 | WriteBuf int
162 | WriteBufSize int
163 | }
164 |
165 | // Websocket is websocket config.
166 | type Websocket struct {
167 | Bind []string
168 | TLSOpen bool
169 | TLSBind []string
170 | CertFile string
171 | PrivateFile string
172 | }
173 |
174 | // Protocol is protocol config.
175 | type Protocol struct {
176 | Timer int
177 | TimerSize int
178 | SvrProto int
179 | CliProto int
180 | HandshakeTimeout xtime.Duration
181 | }
182 |
183 | // Bucket is bucket config.
184 | type Bucket struct {
185 | Size int
186 | Channel int
187 | Room int
188 | RoutineAmount uint64
189 | RoutineSize int
190 | }
191 |
192 | // Whitelist is white list config.
193 | type Whitelist struct {
194 | Whitelist []int64
195 | WhiteLog string
196 | }
197 |
--------------------------------------------------------------------------------
/internal/job/comet.go:
--------------------------------------------------------------------------------
1 | package job
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/url"
7 | "sync/atomic"
8 | "time"
9 |
10 | "github.com/Terry-Mao/goim/api/comet"
11 | "github.com/Terry-Mao/goim/internal/job/conf"
12 | "github.com/bilibili/discovery/naming"
13 |
14 | log "github.com/golang/glog"
15 | "google.golang.org/grpc"
16 | "google.golang.org/grpc/keepalive"
17 | )
18 |
19 | var (
20 | // grpc options
21 | grpcKeepAliveTime = time.Duration(10) * time.Second
22 | grpcKeepAliveTimeout = time.Duration(3) * time.Second
23 | grpcBackoffMaxDelay = time.Duration(3) * time.Second
24 | grpcMaxSendMsgSize = 1 << 24
25 | grpcMaxCallMsgSize = 1 << 24
26 | )
27 |
28 | const (
29 | // grpc options
30 | grpcInitialWindowSize = 1 << 24
31 | grpcInitialConnWindowSize = 1 << 24
32 | )
33 |
34 | func newCometClient(addr string) (comet.CometClient, error) {
35 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second))
36 | defer cancel()
37 | conn, err := grpc.DialContext(ctx, addr,
38 | []grpc.DialOption{
39 | grpc.WithInsecure(),
40 | grpc.WithInitialWindowSize(grpcInitialWindowSize),
41 | grpc.WithInitialConnWindowSize(grpcInitialConnWindowSize),
42 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(grpcMaxCallMsgSize)),
43 | grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(grpcMaxSendMsgSize)),
44 | grpc.WithBackoffMaxDelay(grpcBackoffMaxDelay),
45 | grpc.WithKeepaliveParams(keepalive.ClientParameters{
46 | Time: grpcKeepAliveTime,
47 | Timeout: grpcKeepAliveTimeout,
48 | PermitWithoutStream: true,
49 | }),
50 | }...,
51 | )
52 | if err != nil {
53 | return nil, err
54 | }
55 | return comet.NewCometClient(conn), err
56 | }
57 |
58 | // Comet is a comet.
59 | type Comet struct {
60 | serverID string
61 | client comet.CometClient
62 | pushChan []chan *comet.PushMsgReq
63 | roomChan []chan *comet.BroadcastRoomReq
64 | broadcastChan chan *comet.BroadcastReq
65 | pushChanNum uint64
66 | roomChanNum uint64
67 | routineSize uint64
68 |
69 | ctx context.Context
70 | cancel context.CancelFunc
71 | }
72 |
73 | // NewComet new a comet.
74 | func NewComet(in *naming.Instance, c *conf.Comet) (*Comet, error) {
75 | cmt := &Comet{
76 | serverID: in.Hostname,
77 | pushChan: make([]chan *comet.PushMsgReq, c.RoutineSize),
78 | roomChan: make([]chan *comet.BroadcastRoomReq, c.RoutineSize),
79 | broadcastChan: make(chan *comet.BroadcastReq, c.RoutineSize),
80 | routineSize: uint64(c.RoutineSize),
81 | }
82 | var grpcAddr string
83 | for _, addrs := range in.Addrs {
84 | u, err := url.Parse(addrs)
85 | if err == nil && u.Scheme == "grpc" {
86 | grpcAddr = u.Host
87 | }
88 | }
89 | if grpcAddr == "" {
90 | return nil, fmt.Errorf("invalid grpc address:%v", in.Addrs)
91 | }
92 | var err error
93 | if cmt.client, err = newCometClient(grpcAddr); err != nil {
94 | return nil, err
95 | }
96 | cmt.ctx, cmt.cancel = context.WithCancel(context.Background())
97 |
98 | for i := 0; i < c.RoutineSize; i++ {
99 | cmt.pushChan[i] = make(chan *comet.PushMsgReq, c.RoutineChan)
100 | cmt.roomChan[i] = make(chan *comet.BroadcastRoomReq, c.RoutineChan)
101 | go cmt.process(cmt.pushChan[i], cmt.roomChan[i], cmt.broadcastChan)
102 | }
103 | return cmt, nil
104 | }
105 |
106 | // Push push a user message.
107 | func (c *Comet) Push(arg *comet.PushMsgReq) (err error) {
108 | idx := atomic.AddUint64(&c.pushChanNum, 1) % c.routineSize
109 | c.pushChan[idx] <- arg
110 | return
111 | }
112 |
113 | // BroadcastRoom broadcast a room message.
114 | func (c *Comet) BroadcastRoom(arg *comet.BroadcastRoomReq) (err error) {
115 | idx := atomic.AddUint64(&c.roomChanNum, 1) % c.routineSize
116 | c.roomChan[idx] <- arg
117 | return
118 | }
119 |
120 | // Broadcast broadcast a message.
121 | func (c *Comet) Broadcast(arg *comet.BroadcastReq) (err error) {
122 | c.broadcastChan <- arg
123 | return
124 | }
125 |
126 | func (c *Comet) process(pushChan chan *comet.PushMsgReq, roomChan chan *comet.BroadcastRoomReq, broadcastChan chan *comet.BroadcastReq) {
127 | for {
128 | select {
129 | case broadcastArg := <-broadcastChan:
130 | _, err := c.client.Broadcast(context.Background(), &comet.BroadcastReq{
131 | Proto: broadcastArg.Proto,
132 | ProtoOp: broadcastArg.ProtoOp,
133 | Speed: broadcastArg.Speed,
134 | })
135 | if err != nil {
136 | log.Errorf("c.client.Broadcast(%s, reply) serverId:%s error(%v)", broadcastArg, c.serverID, err)
137 | }
138 | case roomArg := <-roomChan:
139 | _, err := c.client.BroadcastRoom(context.Background(), &comet.BroadcastRoomReq{
140 | RoomID: roomArg.RoomID,
141 | Proto: roomArg.Proto,
142 | })
143 | if err != nil {
144 | log.Errorf("c.client.BroadcastRoom(%s, reply) serverId:%s error(%v)", roomArg, c.serverID, err)
145 | }
146 | case pushArg := <-pushChan:
147 | _, err := c.client.PushMsg(context.Background(), &comet.PushMsgReq{
148 | Keys: pushArg.Keys,
149 | Proto: pushArg.Proto,
150 | ProtoOp: pushArg.ProtoOp,
151 | })
152 | if err != nil {
153 | log.Errorf("c.client.PushMsg(%s, reply) serverId:%s error(%v)", pushArg, c.serverID, err)
154 | }
155 | case <-c.ctx.Done():
156 | return
157 | }
158 | }
159 | }
160 |
161 | // Close close the resources.
162 | func (c *Comet) Close() (err error) {
163 | finish := make(chan bool)
164 | go func() {
165 | for {
166 | n := len(c.broadcastChan)
167 | for _, ch := range c.pushChan {
168 | n += len(ch)
169 | }
170 | for _, ch := range c.roomChan {
171 | n += len(ch)
172 | }
173 | if n == 0 {
174 | finish <- true
175 | return
176 | }
177 | time.Sleep(time.Second)
178 | }
179 | }()
180 | select {
181 | case <-finish:
182 | log.Info("close comet finish")
183 | case <-time.After(5 * time.Second):
184 | err = fmt.Errorf("close comet(server:%s push:%d room:%d broadcast:%d) timeout", c.serverID, len(c.pushChan), len(c.roomChan), len(c.broadcastChan))
185 | }
186 | c.cancel()
187 | return
188 | }
189 |
--------------------------------------------------------------------------------
/internal/comet/bucket.go:
--------------------------------------------------------------------------------
1 | package comet
2 |
3 | import (
4 | "sync"
5 | "sync/atomic"
6 |
7 | pb "github.com/Terry-Mao/goim/api/comet"
8 | "github.com/Terry-Mao/goim/api/protocol"
9 | "github.com/Terry-Mao/goim/internal/comet/conf"
10 | )
11 |
12 | // Bucket is a channel holder.
13 | type Bucket struct {
14 | c *conf.Bucket
15 | cLock sync.RWMutex // protect the channels for chs
16 | chs map[string]*Channel // map sub key to a channel
17 | // room
18 | rooms map[string]*Room // bucket room channels
19 | routines []chan *pb.BroadcastRoomReq
20 | routinesNum uint64
21 |
22 | ipCnts map[string]int32
23 | }
24 |
25 | // NewBucket new a bucket struct. store the key with im channel.
26 | func NewBucket(c *conf.Bucket) (b *Bucket) {
27 | b = new(Bucket)
28 | b.chs = make(map[string]*Channel, c.Channel)
29 | b.ipCnts = make(map[string]int32)
30 | b.c = c
31 | b.rooms = make(map[string]*Room, c.Room)
32 | b.routines = make([]chan *pb.BroadcastRoomReq, c.RoutineAmount)
33 | for i := uint64(0); i < c.RoutineAmount; i++ {
34 | c := make(chan *pb.BroadcastRoomReq, c.RoutineSize)
35 | b.routines[i] = c
36 | go b.roomproc(c)
37 | }
38 | return
39 | }
40 |
41 | // ChannelCount channel count in the bucket
42 | func (b *Bucket) ChannelCount() int {
43 | return len(b.chs)
44 | }
45 |
46 | // RoomCount room count in the bucket
47 | func (b *Bucket) RoomCount() int {
48 | return len(b.rooms)
49 | }
50 |
51 | // RoomsCount get all room id where online number > 0.
52 | func (b *Bucket) RoomsCount() (res map[string]int32) {
53 | var (
54 | roomID string
55 | room *Room
56 | )
57 | b.cLock.RLock()
58 | res = make(map[string]int32)
59 | for roomID, room = range b.rooms {
60 | if room.Online > 0 {
61 | res[roomID] = room.Online
62 | }
63 | }
64 | b.cLock.RUnlock()
65 | return
66 | }
67 |
68 | // ChangeRoom change ro room
69 | func (b *Bucket) ChangeRoom(nrid string, ch *Channel) (err error) {
70 | var (
71 | nroom *Room
72 | ok bool
73 | oroom = ch.Room
74 | )
75 | // change to no room
76 | if nrid == "" {
77 | if oroom != nil && oroom.Del(ch) {
78 | b.DelRoom(oroom)
79 | }
80 | ch.Room = nil
81 | return
82 | }
83 | b.cLock.Lock()
84 | if nroom, ok = b.rooms[nrid]; !ok {
85 | nroom = NewRoom(nrid)
86 | b.rooms[nrid] = nroom
87 | }
88 | b.cLock.Unlock()
89 | if oroom != nil && oroom.Del(ch) {
90 | b.DelRoom(oroom)
91 | }
92 |
93 | if err = nroom.Put(ch); err != nil {
94 | return
95 | }
96 | ch.Room = nroom
97 | return
98 | }
99 |
100 | // Put put a channel according with sub key.
101 | func (b *Bucket) Put(rid string, ch *Channel) (err error) {
102 | var (
103 | room *Room
104 | ok bool
105 | )
106 | b.cLock.Lock()
107 | // close old channel
108 | if dch := b.chs[ch.Key]; dch != nil {
109 | dch.Close()
110 | }
111 | b.chs[ch.Key] = ch
112 | if rid != "" {
113 | if room, ok = b.rooms[rid]; !ok {
114 | room = NewRoom(rid)
115 | b.rooms[rid] = room
116 | }
117 | ch.Room = room
118 | }
119 | b.ipCnts[ch.IP]++
120 | b.cLock.Unlock()
121 | if room != nil {
122 | err = room.Put(ch)
123 | }
124 | return
125 | }
126 |
127 | // Del delete the channel by sub key.
128 | func (b *Bucket) Del(dch *Channel) {
129 | room := dch.Room
130 | b.cLock.Lock()
131 | if ch, ok := b.chs[dch.Key]; ok {
132 | if ch == dch {
133 | delete(b.chs, ch.Key)
134 | }
135 | // ip counter
136 | if b.ipCnts[ch.IP] > 1 {
137 | b.ipCnts[ch.IP]--
138 | } else {
139 | delete(b.ipCnts, ch.IP)
140 | }
141 | }
142 | b.cLock.Unlock()
143 | if room != nil && room.Del(dch) {
144 | // if empty room, must delete from bucket
145 | b.DelRoom(room)
146 | }
147 | }
148 |
149 | // Channel get a channel by sub key.
150 | func (b *Bucket) Channel(key string) (ch *Channel) {
151 | b.cLock.RLock()
152 | ch = b.chs[key]
153 | b.cLock.RUnlock()
154 | return
155 | }
156 |
157 | // Broadcast push msgs to all channels in the bucket.
158 | func (b *Bucket) Broadcast(p *protocol.Proto, op int32) {
159 | var ch *Channel
160 | b.cLock.RLock()
161 | for _, ch = range b.chs {
162 | if !ch.NeedPush(op) {
163 | continue
164 | }
165 | _ = ch.Push(p)
166 | }
167 | b.cLock.RUnlock()
168 | }
169 |
170 | // Room get a room by roomid.
171 | func (b *Bucket) Room(rid string) (room *Room) {
172 | b.cLock.RLock()
173 | room = b.rooms[rid]
174 | b.cLock.RUnlock()
175 | return
176 | }
177 |
178 | // DelRoom delete a room by roomid.
179 | func (b *Bucket) DelRoom(room *Room) {
180 | b.cLock.Lock()
181 | delete(b.rooms, room.ID)
182 | b.cLock.Unlock()
183 | room.Close()
184 | }
185 |
186 | // BroadcastRoom broadcast a message to specified room
187 | func (b *Bucket) BroadcastRoom(arg *pb.BroadcastRoomReq) {
188 | num := atomic.AddUint64(&b.routinesNum, 1) % b.c.RoutineAmount
189 | b.routines[num] <- arg
190 | }
191 |
192 | // Rooms get all room id where online number > 0.
193 | func (b *Bucket) Rooms() (res map[string]struct{}) {
194 | var (
195 | roomID string
196 | room *Room
197 | )
198 | res = make(map[string]struct{})
199 | b.cLock.RLock()
200 | for roomID, room = range b.rooms {
201 | if room.Online > 0 {
202 | res[roomID] = struct{}{}
203 | }
204 | }
205 | b.cLock.RUnlock()
206 | return
207 | }
208 |
209 | // IPCount get ip count.
210 | func (b *Bucket) IPCount() (res map[string]struct{}) {
211 | var (
212 | ip string
213 | )
214 | b.cLock.RLock()
215 | res = make(map[string]struct{}, len(b.ipCnts))
216 | for ip = range b.ipCnts {
217 | res[ip] = struct{}{}
218 | }
219 | b.cLock.RUnlock()
220 | return
221 | }
222 |
223 | // UpRoomsCount update all room count
224 | func (b *Bucket) UpRoomsCount(roomCountMap map[string]int32) {
225 | var (
226 | roomID string
227 | room *Room
228 | )
229 | b.cLock.RLock()
230 | for roomID, room = range b.rooms {
231 | room.AllOnline = roomCountMap[roomID]
232 | }
233 | b.cLock.RUnlock()
234 | }
235 |
236 | // roomproc
237 | func (b *Bucket) roomproc(c chan *pb.BroadcastRoomReq) {
238 | for {
239 | arg := <-c
240 | if room := b.Room(arg.RoomID); room != nil {
241 | room.Push(arg.Proto)
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/pkg/time/timer.go:
--------------------------------------------------------------------------------
1 | package time
2 |
3 | import (
4 | "sync"
5 | itime "time"
6 |
7 | log "github.com/golang/glog"
8 | )
9 |
10 | const (
11 | timerFormat = "2006-01-02 15:04:05"
12 | infiniteDuration = itime.Duration(1<<63 - 1)
13 | )
14 |
15 | // TimerData timer data.
16 | type TimerData struct {
17 | Key string
18 | expire itime.Time
19 | fn func()
20 | index int
21 | next *TimerData
22 | }
23 |
24 | // Delay delay duration.
25 | func (td *TimerData) Delay() itime.Duration {
26 | return itime.Until(td.expire)
27 | }
28 |
29 | // ExpireString expire string.
30 | func (td *TimerData) ExpireString() string {
31 | return td.expire.Format(timerFormat)
32 | }
33 |
34 | // Timer timer.
35 | type Timer struct {
36 | lock sync.Mutex
37 | free *TimerData
38 | timers []*TimerData
39 | signal *itime.Timer
40 | num int
41 | }
42 |
43 | // NewTimer new a timer.
44 | // A heap must be initialized before any of the heap operations
45 | // can be used. Init is idempotent with respect to the heap invariants
46 | // and may be called whenever the heap invariants may have been invalidated.
47 | // Its complexity is O(n) where n = h.Len().
48 | //
49 | func NewTimer(num int) (t *Timer) {
50 | t = new(Timer)
51 | t.init(num)
52 | return t
53 | }
54 |
55 | // Init init the timer.
56 | func (t *Timer) Init(num int) {
57 | t.init(num)
58 | }
59 |
60 | func (t *Timer) init(num int) {
61 | t.signal = itime.NewTimer(infiniteDuration)
62 | t.timers = make([]*TimerData, 0, num)
63 | t.num = num
64 | t.grow()
65 | go t.start()
66 | }
67 |
68 | func (t *Timer) grow() {
69 | var (
70 | i int
71 | td *TimerData
72 | tds = make([]TimerData, t.num)
73 | )
74 | t.free = &(tds[0])
75 | td = t.free
76 | for i = 1; i < t.num; i++ {
77 | td.next = &(tds[i])
78 | td = td.next
79 | }
80 | td.next = nil
81 | }
82 |
83 | // get get a free timer data.
84 | func (t *Timer) get() (td *TimerData) {
85 | if td = t.free; td == nil {
86 | t.grow()
87 | td = t.free
88 | }
89 | t.free = td.next
90 | return
91 | }
92 |
93 | // put put back a timer data.
94 | func (t *Timer) put(td *TimerData) {
95 | td.fn = nil
96 | td.next = t.free
97 | t.free = td
98 | }
99 |
100 | // Add add the element x onto the heap. The complexity is
101 | // O(log(n)) where n = h.Len().
102 | func (t *Timer) Add(expire itime.Duration, fn func()) (td *TimerData) {
103 | t.lock.Lock()
104 | td = t.get()
105 | td.expire = itime.Now().Add(expire)
106 | td.fn = fn
107 | t.add(td)
108 | t.lock.Unlock()
109 | return
110 | }
111 |
112 | // Del removes the element at index i from the heap.
113 | // The complexity is O(log(n)) where n = h.Len().
114 | func (t *Timer) Del(td *TimerData) {
115 | t.lock.Lock()
116 | t.del(td)
117 | t.put(td)
118 | t.lock.Unlock()
119 | }
120 |
121 | // Push pushes the element x onto the heap. The complexity is
122 | // O(log(n)) where n = h.Len().
123 | func (t *Timer) add(td *TimerData) {
124 | var d itime.Duration
125 | td.index = len(t.timers)
126 | // add to the minheap last node
127 | t.timers = append(t.timers, td)
128 | t.up(td.index)
129 | if td.index == 0 {
130 | // if first node, signal start goroutine
131 | d = td.Delay()
132 | t.signal.Reset(d)
133 | if Debug {
134 | log.Infof("timer: add reset delay %d ms", int64(d)/int64(itime.Millisecond))
135 | }
136 | }
137 | if Debug {
138 | log.Infof("timer: push item key: %s, expire: %s, index: %d", td.Key, td.ExpireString(), td.index)
139 | }
140 | }
141 |
142 | func (t *Timer) del(td *TimerData) {
143 | var (
144 | i = td.index
145 | last = len(t.timers) - 1
146 | )
147 | if i < 0 || i > last || t.timers[i] != td {
148 | // already remove, usually by expire
149 | if Debug {
150 | log.Infof("timer del i: %d, last: %d, %p", i, last, td)
151 | }
152 | return
153 | }
154 | if i != last {
155 | t.swap(i, last)
156 | t.down(i, last)
157 | t.up(i)
158 | }
159 | // remove item is the last node
160 | t.timers[last].index = -1 // for safety
161 | t.timers = t.timers[:last]
162 | if Debug {
163 | log.Infof("timer: remove item key: %s, expire: %s, index: %d", td.Key, td.ExpireString(), td.index)
164 | }
165 | }
166 |
167 | // Set update timer data.
168 | func (t *Timer) Set(td *TimerData, expire itime.Duration) {
169 | t.lock.Lock()
170 | t.del(td)
171 | td.expire = itime.Now().Add(expire)
172 | t.add(td)
173 | t.lock.Unlock()
174 | }
175 |
176 | // start start the timer.
177 | func (t *Timer) start() {
178 | for {
179 | t.expire()
180 | <-t.signal.C
181 | }
182 | }
183 |
184 | // expire removes the minimum element (according to Less) from the heap.
185 | // The complexity is O(log(n)) where n = max.
186 | // It is equivalent to Del(0).
187 | func (t *Timer) expire() {
188 | var (
189 | fn func()
190 | td *TimerData
191 | d itime.Duration
192 | )
193 | t.lock.Lock()
194 | for {
195 | if len(t.timers) == 0 {
196 | d = infiniteDuration
197 | if Debug {
198 | log.Info("timer: no other instance")
199 | }
200 | break
201 | }
202 | td = t.timers[0]
203 | if d = td.Delay(); d > 0 {
204 | break
205 | }
206 | fn = td.fn
207 | // let caller put back
208 | t.del(td)
209 | t.lock.Unlock()
210 | if fn == nil {
211 | log.Warning("expire timer no fn")
212 | } else {
213 | if Debug {
214 | log.Infof("timer key: %s, expire: %s, index: %d expired, call fn", td.Key, td.ExpireString(), td.index)
215 | }
216 | fn()
217 | }
218 | t.lock.Lock()
219 | }
220 | t.signal.Reset(d)
221 | if Debug {
222 | log.Infof("timer: expier reset delay %d ms", int64(d)/int64(itime.Millisecond))
223 | }
224 | t.lock.Unlock()
225 | }
226 |
227 | func (t *Timer) up(j int) {
228 | for {
229 | i := (j - 1) / 2 // parent
230 | if i >= j || !t.less(j, i) {
231 | break
232 | }
233 | t.swap(i, j)
234 | j = i
235 | }
236 | }
237 |
238 | func (t *Timer) down(i, n int) {
239 | for {
240 | j1 := 2*i + 1
241 | if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
242 | break
243 | }
244 | j := j1 // left child
245 | if j2 := j1 + 1; j2 < n && !t.less(j1, j2) {
246 | j = j2 // = 2*i + 2 // right child
247 | }
248 | if !t.less(j, i) {
249 | break
250 | }
251 | t.swap(i, j)
252 | i = j
253 | }
254 | }
255 |
256 | func (t *Timer) less(i, j int) bool {
257 | return t.timers[i].expire.Before(t.timers[j].expire)
258 | }
259 |
260 | func (t *Timer) swap(i, j int) {
261 | t.timers[i], t.timers[j] = t.timers[j], t.timers[i]
262 | t.timers[i].index = i
263 | t.timers[j].index = j
264 | }
265 |
--------------------------------------------------------------------------------
/benchmarks/client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Start Commond eg: ./client 1 1000 localhost:3101
4 | // first parameter:beginning userId
5 | // second parameter: amount of clients
6 | // third parameter: comet server ip
7 |
8 | import (
9 | "bufio"
10 | "encoding/binary"
11 | "encoding/json"
12 | "flag"
13 | "fmt"
14 | "math/rand"
15 | "net"
16 | "os"
17 | "runtime"
18 | "strconv"
19 | "sync/atomic"
20 | "time"
21 |
22 | log "github.com/golang/glog"
23 | )
24 |
25 | const (
26 | opHeartbeat = int32(2)
27 | opHeartbeatReply = int32(3)
28 | opAuth = int32(7)
29 | opAuthReply = int32(8)
30 | )
31 |
32 | const (
33 | rawHeaderLen = uint16(16)
34 | heart = 240 * time.Second
35 | )
36 |
37 | // Proto proto.
38 | type Proto struct {
39 | PackLen int32 // package length
40 | HeaderLen int16 // header length
41 | Ver int16 // protocol version
42 | Operation int32 // operation for request
43 | Seq int32 // sequence number chosen by client
44 | Body []byte // body
45 | }
46 |
47 | // AuthToken auth token.
48 | type AuthToken struct {
49 | Mid int64 `json:"mid"`
50 | Key string `json:"key"`
51 | RoomID string `json:"room_id"`
52 | Platform string `json:"platform"`
53 | Accepts []int32 `json:"accepts"`
54 | }
55 |
56 | var (
57 | countDown int64
58 | aliveCount int64
59 | )
60 |
61 | func main() {
62 | runtime.GOMAXPROCS(runtime.NumCPU())
63 | flag.Parse()
64 | begin, err := strconv.Atoi(os.Args[1])
65 | if err != nil {
66 | panic(err)
67 | }
68 | num, err := strconv.Atoi(os.Args[2])
69 | if err != nil {
70 | panic(err)
71 | }
72 | go result()
73 | for i := begin; i < begin+num; i++ {
74 | go client(int64(i))
75 | }
76 | // signal
77 | var exit chan bool
78 | <-exit
79 | }
80 |
81 | func result() {
82 | var (
83 | lastTimes int64
84 | interval = int64(5)
85 | )
86 | for {
87 | nowCount := atomic.LoadInt64(&countDown)
88 | nowAlive := atomic.LoadInt64(&aliveCount)
89 | diff := nowCount - lastTimes
90 | lastTimes = nowCount
91 | fmt.Println(fmt.Sprintf("%s alive:%d down:%d down/s:%d", time.Now().Format("2006-01-02 15:04:05"), nowAlive, nowCount, diff/interval))
92 | time.Sleep(time.Second * time.Duration(interval))
93 | }
94 | }
95 |
96 | func client(mid int64) {
97 | for {
98 | startClient(mid)
99 | time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
100 | }
101 | }
102 |
103 | func startClient(key int64) {
104 | time.Sleep(time.Duration(rand.Intn(120)) * time.Second)
105 | atomic.AddInt64(&aliveCount, 1)
106 | quit := make(chan bool, 1)
107 | defer func() {
108 | close(quit)
109 | atomic.AddInt64(&aliveCount, -1)
110 | }()
111 | // connnect to server
112 | conn, err := net.Dial("tcp", os.Args[3])
113 | if err != nil {
114 | log.Errorf("net.Dial(%s) error(%v)", os.Args[3], err)
115 | return
116 | }
117 | seq := int32(0)
118 | wr := bufio.NewWriter(conn)
119 | rd := bufio.NewReader(conn)
120 | authToken := &AuthToken{
121 | key,
122 | "",
123 | "test://1",
124 | "ios",
125 | []int32{1000, 1001, 1002},
126 | }
127 | proto := new(Proto)
128 | proto.Ver = 1
129 | proto.Operation = opAuth
130 | proto.Seq = seq
131 | proto.Body, _ = json.Marshal(authToken)
132 | if err = tcpWriteProto(wr, proto); err != nil {
133 | log.Errorf("tcpWriteProto() error(%v)", err)
134 | return
135 | }
136 | if err = tcpReadProto(rd, proto); err != nil {
137 | log.Errorf("tcpReadProto() error(%v)", err)
138 | return
139 | }
140 | log.Infof("key:%d auth ok, proto: %v", key, proto)
141 | seq++
142 | // writer
143 | go func() {
144 | hbProto := new(Proto)
145 | for {
146 | // heartbeat
147 | hbProto.Operation = opHeartbeat
148 | hbProto.Seq = seq
149 | hbProto.Body = nil
150 | if err = tcpWriteProto(wr, hbProto); err != nil {
151 | log.Errorf("key:%d tcpWriteProto() error(%v)", key, err)
152 | return
153 | }
154 | log.Infof("key:%d Write heartbeat", key)
155 | time.Sleep(heart)
156 | seq++
157 | select {
158 | case <-quit:
159 | return
160 | default:
161 | }
162 | }
163 | }()
164 | // reader
165 | for {
166 | if err = tcpReadProto(rd, proto); err != nil {
167 | log.Errorf("key:%d tcpReadProto() error(%v)", key, err)
168 | quit <- true
169 | return
170 | }
171 | if proto.Operation == opAuthReply {
172 | log.Infof("key:%d auth success", key)
173 | } else if proto.Operation == opHeartbeatReply {
174 | log.Infof("key:%d receive heartbeat", key)
175 | if err = conn.SetReadDeadline(time.Now().Add(heart + 60*time.Second)); err != nil {
176 | log.Errorf("conn.SetReadDeadline() error(%v)", err)
177 | quit <- true
178 | return
179 | }
180 | } else {
181 | log.Infof("key:%d op:%d msg: %s", key, proto.Operation, string(proto.Body))
182 | atomic.AddInt64(&countDown, 1)
183 | }
184 | }
185 | }
186 |
187 | func tcpWriteProto(wr *bufio.Writer, proto *Proto) (err error) {
188 | // write
189 | if err = binary.Write(wr, binary.BigEndian, uint32(rawHeaderLen)+uint32(len(proto.Body))); err != nil {
190 | return
191 | }
192 | if err = binary.Write(wr, binary.BigEndian, rawHeaderLen); err != nil {
193 | return
194 | }
195 | if err = binary.Write(wr, binary.BigEndian, proto.Ver); err != nil {
196 | return
197 | }
198 | if err = binary.Write(wr, binary.BigEndian, proto.Operation); err != nil {
199 | return
200 | }
201 | if err = binary.Write(wr, binary.BigEndian, proto.Seq); err != nil {
202 | return
203 | }
204 | if proto.Body != nil {
205 | if err = binary.Write(wr, binary.BigEndian, proto.Body); err != nil {
206 | return
207 | }
208 | }
209 | err = wr.Flush()
210 | return
211 | }
212 |
213 | func tcpReadProto(rd *bufio.Reader, proto *Proto) (err error) {
214 | var (
215 | packLen int32
216 | headerLen int16
217 | )
218 | // read
219 | if err = binary.Read(rd, binary.BigEndian, &packLen); err != nil {
220 | return
221 | }
222 | if err = binary.Read(rd, binary.BigEndian, &headerLen); err != nil {
223 | return
224 | }
225 | if err = binary.Read(rd, binary.BigEndian, &proto.Ver); err != nil {
226 | return
227 | }
228 | if err = binary.Read(rd, binary.BigEndian, &proto.Operation); err != nil {
229 | return
230 | }
231 | if err = binary.Read(rd, binary.BigEndian, &proto.Seq); err != nil {
232 | return
233 | }
234 | var (
235 | n, t int
236 | bodyLen = int(packLen - int32(headerLen))
237 | )
238 | if bodyLen > 0 {
239 | proto.Body = make([]byte, bodyLen)
240 | for {
241 | if t, err = rd.Read(proto.Body[n:]); err != nil {
242 | return
243 | }
244 | if n += t; n == bodyLen {
245 | break
246 | }
247 | }
248 | } else {
249 | proto.Body = nil
250 | }
251 | return
252 | }
253 |
--------------------------------------------------------------------------------
/pkg/websocket/conn.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "encoding/binary"
5 | "errors"
6 | "fmt"
7 | "io"
8 |
9 | "github.com/Terry-Mao/goim/pkg/bufio"
10 | )
11 |
12 | const (
13 | // Frame header byte 0 bits from Section 5.2 of RFC 6455
14 | finBit = 1 << 7
15 | rsv1Bit = 1 << 6
16 | rsv2Bit = 1 << 5
17 | rsv3Bit = 1 << 4
18 | opBit = 0x0f
19 |
20 | // Frame header byte 1 bits from Section 5.2 of RFC 6455
21 | maskBit = 1 << 7
22 | lenBit = 0x7f
23 |
24 | continuationFrame = 0
25 | continuationFrameMaxRead = 100
26 | )
27 |
28 | // The message types are defined in RFC 6455, section 11.8.
29 | const (
30 | // TextMessage denotes a text data message. The text message payload is
31 | // interpreted as UTF-8 encoded text data.
32 | TextMessage = 1
33 |
34 | // BinaryMessage denotes a binary data message.
35 | BinaryMessage = 2
36 |
37 | // CloseMessage denotes a close control message. The optional message
38 | // payload contains a numeric code and text. Use the FormatCloseMessage
39 | // function to format a close message payload.
40 | CloseMessage = 8
41 |
42 | // PingMessage denotes a ping control message. The optional message payload
43 | // is UTF-8 encoded text.
44 | PingMessage = 9
45 |
46 | // PongMessage denotes a ping control message. The optional message payload
47 | // is UTF-8 encoded text.
48 | PongMessage = 10
49 | )
50 |
51 | var (
52 | // ErrMessageClose close control message
53 | ErrMessageClose = errors.New("close control message")
54 | // ErrMessageMaxRead continuation frame max read
55 | ErrMessageMaxRead = errors.New("continuation frame max read")
56 | )
57 |
58 | // Conn represents a WebSocket connection.
59 | type Conn struct {
60 | rwc io.ReadWriteCloser
61 | r *bufio.Reader
62 | w *bufio.Writer
63 | maskKey []byte
64 | }
65 |
66 | // new connection
67 | func newConn(rwc io.ReadWriteCloser, r *bufio.Reader, w *bufio.Writer) *Conn {
68 | return &Conn{rwc: rwc, r: r, w: w, maskKey: make([]byte, 4)}
69 | }
70 |
71 | // WriteMessage write a message by type.
72 | func (c *Conn) WriteMessage(msgType int, msg []byte) (err error) {
73 | if err = c.WriteHeader(msgType, len(msg)); err != nil {
74 | return
75 | }
76 | err = c.WriteBody(msg)
77 | return
78 | }
79 |
80 | // WriteHeader write header frame.
81 | func (c *Conn) WriteHeader(msgType int, length int) (err error) {
82 | var h []byte
83 | if h, err = c.w.Peek(2); err != nil {
84 | return
85 | }
86 | // 1.First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits)
87 | h[0] = 0
88 | h[0] |= finBit | byte(msgType)
89 | // 2.Second byte. Mask/Payload len(7bits)
90 | h[1] = 0
91 | switch {
92 | case length <= 125:
93 | // 7 bits
94 | h[1] |= byte(length)
95 | case length < 65536:
96 | // 16 bits
97 | h[1] |= 126
98 | if h, err = c.w.Peek(2); err != nil {
99 | return
100 | }
101 | binary.BigEndian.PutUint16(h, uint16(length))
102 | default:
103 | // 64 bits
104 | h[1] |= 127
105 | if h, err = c.w.Peek(8); err != nil {
106 | return
107 | }
108 | binary.BigEndian.PutUint64(h, uint64(length))
109 | }
110 | return
111 | }
112 |
113 | // WriteBody write a message body.
114 | func (c *Conn) WriteBody(b []byte) (err error) {
115 | if len(b) > 0 {
116 | _, err = c.w.Write(b)
117 | }
118 | return
119 | }
120 |
121 | // Peek write peek.
122 | func (c *Conn) Peek(n int) ([]byte, error) {
123 | return c.w.Peek(n)
124 | }
125 |
126 | // Flush flush writer buffer
127 | func (c *Conn) Flush() error {
128 | return c.w.Flush()
129 | }
130 |
131 | // ReadMessage read a message.
132 | func (c *Conn) ReadMessage() (op int, payload []byte, err error) {
133 | var (
134 | fin bool
135 | finOp, n int
136 | partPayload []byte
137 | )
138 | for {
139 | // read frame
140 | if fin, op, partPayload, err = c.readFrame(); err != nil {
141 | return
142 | }
143 | switch op {
144 | case BinaryMessage, TextMessage, continuationFrame:
145 | if fin && len(payload) == 0 {
146 | return op, partPayload, nil
147 | }
148 | // continuation frame
149 | payload = append(payload, partPayload...)
150 | if op != continuationFrame {
151 | finOp = op
152 | }
153 | // final frame
154 | if fin {
155 | op = finOp
156 | return
157 | }
158 | case PingMessage:
159 | // handler ping
160 | if err = c.WriteMessage(PongMessage, partPayload); err != nil {
161 | return
162 | }
163 | case PongMessage:
164 | // handler pong
165 | case CloseMessage:
166 | // handler close
167 | err = ErrMessageClose
168 | return
169 | default:
170 | err = fmt.Errorf("unknown control message, fin=%t, op=%d", fin, op)
171 | return
172 | }
173 | if n > continuationFrameMaxRead {
174 | err = ErrMessageMaxRead
175 | return
176 | }
177 | n++
178 | }
179 | }
180 |
181 | func (c *Conn) readFrame() (fin bool, op int, payload []byte, err error) {
182 | var (
183 | b byte
184 | p []byte
185 | mask bool
186 | maskKey []byte
187 | payloadLen int64
188 | )
189 | // 1.First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits)
190 | b, err = c.r.ReadByte()
191 | if err != nil {
192 | return
193 | }
194 | // final frame
195 | fin = (b & finBit) != 0
196 | // rsv MUST be 0
197 | if rsv := b & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 {
198 | return false, 0, nil, fmt.Errorf("unexpected reserved bits rsv1=%d, rsv2=%d, rsv3=%d", b&rsv1Bit, b&rsv2Bit, b&rsv3Bit)
199 | }
200 | // op code
201 | op = int(b & opBit)
202 | // 2.Second byte. Mask/Payload len(7bits)
203 | b, err = c.r.ReadByte()
204 | if err != nil {
205 | return
206 | }
207 | // is mask payload
208 | mask = (b & maskBit) != 0
209 | // payload length
210 | switch b & lenBit {
211 | case 126:
212 | // 16 bits
213 | if p, err = c.r.Pop(2); err != nil {
214 | return
215 | }
216 | payloadLen = int64(binary.BigEndian.Uint16(p))
217 | case 127:
218 | // 64 bits
219 | if p, err = c.r.Pop(8); err != nil {
220 | return
221 | }
222 | payloadLen = int64(binary.BigEndian.Uint64(p))
223 | default:
224 | // 7 bits
225 | payloadLen = int64(b & lenBit)
226 | }
227 | // read mask key
228 | if mask {
229 | maskKey, err = c.r.Pop(4)
230 | if err != nil {
231 | return
232 | }
233 | if c.maskKey == nil {
234 | c.maskKey = make([]byte, 4)
235 | }
236 | copy(c.maskKey, maskKey)
237 | }
238 | // read payload
239 | if payloadLen > 0 {
240 | if payload, err = c.r.Pop(int(payloadLen)); err != nil {
241 | return
242 | }
243 | if mask {
244 | maskBytes(c.maskKey, 0, payload)
245 | }
246 | }
247 | return
248 | }
249 |
250 | // Close close the connection.
251 | func (c *Conn) Close() error {
252 | return c.rwc.Close()
253 | }
254 |
255 | func maskBytes(key []byte, pos int, b []byte) int {
256 | for i := range b {
257 | b[i] ^= key[pos&3]
258 | pos++
259 | }
260 | return pos & 3
261 | }
262 |
--------------------------------------------------------------------------------
/examples/javascript/client.js:
--------------------------------------------------------------------------------
1 | (function(win) {
2 | const rawHeaderLen = 16;
3 | const packetOffset = 0;
4 | const headerOffset = 4;
5 | const verOffset = 6;
6 | const opOffset = 8;
7 | const seqOffset = 12;
8 |
9 | var Client = function(options) {
10 | var MAX_CONNECT_TIMES = 10;
11 | var DELAY = 15000;
12 | this.options = options || {};
13 | this.createConnect(MAX_CONNECT_TIMES, DELAY);
14 | }
15 |
16 | var appendMsg = function(text) {
17 | var span = document.createElement("SPAN");
18 | var text = document.createTextNode(text);
19 | span.appendChild(text);
20 | document.getElementById("box").appendChild(span);
21 | }
22 |
23 | Client.prototype.createConnect = function(max, delay) {
24 | var self = this;
25 | if (max === 0) {
26 | return;
27 | }
28 | connect();
29 |
30 | var textDecoder = new TextDecoder();
31 | var textEncoder = new TextEncoder();
32 | var heartbeatInterval;
33 | function connect() {
34 | var ws = new WebSocket('ws://sh.tony.wiki:3102/sub');
35 | //var ws = new WebSocket('ws://127.0.0.1:3102/sub');
36 | ws.binaryType = 'arraybuffer';
37 | ws.onopen = function() {
38 | auth();
39 | }
40 |
41 | ws.onmessage = function(evt) {
42 | var data = evt.data;
43 | var dataView = new DataView(data, 0);
44 | var packetLen = dataView.getInt32(packetOffset);
45 | var headerLen = dataView.getInt16(headerOffset);
46 | var ver = dataView.getInt16(verOffset);
47 | var op = dataView.getInt32(opOffset);
48 | var seq = dataView.getInt32(seqOffset);
49 |
50 | console.log("receiveHeader: packetLen=" + packetLen, "headerLen=" + headerLen, "ver=" + ver, "op=" + op, "seq=" + seq);
51 |
52 | switch(op) {
53 | case 8:
54 | // auth reply ok
55 | document.getElementById("status").innerHTML = "ok";
56 | appendMsg("receive: auth reply");
57 | // send a heartbeat to server
58 | heartbeat();
59 | heartbeatInterval = setInterval(heartbeat, 30 * 1000);
60 | break;
61 | case 3:
62 | // receive a heartbeat from server
63 | console.log("receive: heartbeat");
64 | appendMsg("receive: heartbeat reply");
65 | break;
66 | case 9:
67 | // batch message
68 | for (var offset=rawHeaderLen; offsetfailed";
94 | }
95 |
96 | function heartbeat() {
97 | var headerBuf = new ArrayBuffer(rawHeaderLen);
98 | var headerView = new DataView(headerBuf, 0);
99 | headerView.setInt32(packetOffset, rawHeaderLen);
100 | headerView.setInt16(headerOffset, rawHeaderLen);
101 | headerView.setInt16(verOffset, 1);
102 | headerView.setInt32(opOffset, 2);
103 | headerView.setInt32(seqOffset, 1);
104 | ws.send(headerBuf);
105 | console.log("send: heartbeat");
106 | appendMsg("send: heartbeat");
107 | }
108 |
109 | function auth() {
110 | var token = '{"mid":123, "room_id":"live://1000", "platform":"web", "accepts":[1000,1001,1002]}'
111 | var headerBuf = new ArrayBuffer(rawHeaderLen);
112 | var headerView = new DataView(headerBuf, 0);
113 | var bodyBuf = textEncoder.encode(token);
114 | headerView.setInt32(packetOffset, rawHeaderLen + bodyBuf.byteLength);
115 | headerView.setInt16(headerOffset, rawHeaderLen);
116 | headerView.setInt16(verOffset, 1);
117 | headerView.setInt32(opOffset, 7);
118 | headerView.setInt32(seqOffset, 1);
119 | ws.send(mergeArrayBuffer(headerBuf, bodyBuf));
120 |
121 | appendMsg("send: auth token: " + token);
122 | }
123 |
124 | function messageReceived(ver, body) {
125 | var notify = self.options.notify;
126 | if(notify) notify(body);
127 | console.log("messageReceived:", "ver=" + ver, "body=" + body);
128 | }
129 |
130 | function mergeArrayBuffer(ab1, ab2) {
131 | var u81 = new Uint8Array(ab1),
132 | u82 = new Uint8Array(ab2),
133 | res = new Uint8Array(ab1.byteLength + ab2.byteLength);
134 | res.set(u81, 0);
135 | res.set(u82, ab1.byteLength);
136 | return res.buffer;
137 | }
138 |
139 | function char2ab(str) {
140 | var buf = new ArrayBuffer(str.length);
141 | var bufView = new Uint8Array(buf);
142 | for (var i=0; i