├── .gitignore
├── LICENSE
├── README.md
├── cleaner.go
├── client.go
├── config.go
├── config
├── config.ini
└── log4go.xml
├── errors.go
├── example
├── client
│ ├── chat.html
│ ├── room.html
│ ├── room_list.html
│ └── static
│ │ └── video
│ │ └── iphonex.mp4
└── serv
│ └── main.go
├── kafka.go
├── log.go
├── message_handler.go
├── proto.go
├── room.go
├── static_server.go
├── utils
└── concurrent_list.go
└── ws_server.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | # Test binary, build with `go test -c`
8 | *.test
9 |
10 | # Output of the go coverage tool, specifically when used with LiteIDE
11 | *.out
12 |
13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
14 | .glide/
15 | .idea
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 JWL
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # danmu
2 |
3 | 轻量弹幕(IM)系统,基于GO
4 |
5 | # Require
6 |
7 | `GO` >= `1.8`
8 |
9 | `KAFKA` >= `1.0`
10 |
11 | # Demo
12 |
13 | 本项目为你提供了一个在线demo:[demo](http://danmu.jwlchina.cn)
14 |
15 | # Getting Start
16 |
17 | 安装好 `GOLANG` 环境以及 `KAFKA`
18 |
19 | 获取项目源码
20 | ```bash
21 | go get github.com/kong36088/danmu
22 | ```
23 |
24 | 到项目`config`目录下配置好你的kafka地址以及服务监听地址
25 |
26 | 为 `kafka` 创建一个topic `danmu`
27 | ```bash
28 | kafka-topics.sh --create --topic danmu --zookeeper localhost:2181 --replication-factor 1 --partitions 1
29 | ```
30 |
31 | 本项目为提供了一个使用实例,在完成环境部署后,运行以下命令:
32 | ```bash
33 | $ cd /path/to/your/application
34 |
35 | $ cd example/serv/
36 |
37 | $ go build # or go run main.go
38 |
39 | $ ./serv # run your application
40 | ```
41 |
42 | 服务开启之后,用浏览器访问打开html文件 `example/client/room_list.html` 即可看到服务效果
--------------------------------------------------------------------------------
/cleaner.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | type Cleaner struct {
4 | }
5 |
6 | var (
7 | cleaner *Cleaner
8 | )
9 |
10 | func InitCleaner() error{
11 | cleaner = new(Cleaner)
12 |
13 | return OK
14 | }
15 |
16 | /**
17 | 清除保存的client信息,关闭连接
18 | */
19 | func (cleaner *Cleaner) CleanClient(client *Client) {
20 | client.Close()
21 |
22 | room, err := roomBucket.Get(client.RoomId)
23 | if err == nil {
24 | delete(room.clients, client.Conn)
25 | }
26 |
27 | clientBucket.Remove(client.Conn)
28 |
29 | client = nil //for gc
30 | }
31 |
32 | //TODO CleanRoom
33 | func (cleaner *Cleaner) CleanRoom(room *Room){
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | "encoding/json"
5 | log "github.com/alecthomas/log4go"
6 | "github.com/gorilla/websocket"
7 | "strconv"
8 | "sync"
9 | "time"
10 | )
11 |
12 | type (
13 | cid int64
14 | )
15 |
16 | var (
17 | clientBucket *ClientBucket
18 | keepalive int
19 | )
20 |
21 | type Client struct {
22 | ClientId cid //remain
23 | RoomId rid
24 | Conn *websocket.Conn
25 | }
26 |
27 | func NewClient(roomId rid, conn *websocket.Conn) *Client {
28 | c := new(Client)
29 | c.RoomId = roomId
30 | c.Conn = conn
31 | return c
32 | }
33 |
34 | func (client *Client) ReadJSON(v interface{}) error {
35 | err := client.Conn.ReadJSON(v)
36 | return err
37 | }
38 |
39 | func (client *Client) Read(v interface{}) (int, []byte, error) {
40 | msgType, msg, err := client.Conn.ReadMessage()
41 | return msgType, msg, err
42 | }
43 |
44 | func (client *Client) WriteErrorMsg(msg string) error {
45 | err := client.Conn.WriteJSON(Proto{
46 | OP: OPErr,
47 | Message: msg,
48 | RoomId: -1,
49 | })
50 | return err
51 | }
52 |
53 | func (client *Client) WriteMessage(msg string, roomId rid) error {
54 | err := client.Conn.WriteJSON(Proto{
55 | OP: OPMsg,
56 | Message: msg,
57 | RoomId: roomId,
58 | })
59 | return err
60 | }
61 |
62 | func (client *Client) WriteControl(messageType int, data []byte, deadline time.Time) error {
63 | return client.Conn.WriteControl(messageType, data, deadline)
64 | }
65 |
66 | func (client *Client) Write(proto *Proto) error {
67 | err := client.Conn.WriteJSON(proto)
68 | return err
69 | }
70 |
71 | func (client *Client) BatchWrite(proto []*Proto) error {
72 | jsonProtos, err := json.Marshal(proto)
73 | if err != nil {
74 | return err
75 | }
76 | err = client.Conn.WriteMessage(websocket.TextMessage, jsonProtos)
77 | return err
78 | }
79 |
80 | func (client *Client) Close() error {
81 | return client.Conn.Close()
82 | }
83 |
84 | func (client *Client) ErrorReport(err error, msg string) error {
85 | if msg != "" {
86 | return client.WriteErrorMsg(msg)
87 | }
88 | return OK
89 | }
90 |
91 | //keepAlive send ping message to client
92 | // should be called by goroutines
93 | func (client *Client) keepAlive() {
94 | if keepalive <= 0 {
95 | return
96 | }
97 | timeout := time.Duration(keepalive) * time.Second
98 |
99 | lastResponse := time.Now()
100 | client.Conn.SetPongHandler(func(msg string) error {
101 | lastResponse = time.Now()
102 |
103 | return nil
104 | })
105 | go func() {
106 | for {
107 | err := client.WriteControl(websocket.PingMessage, []byte("keepalive"), time.Now().Add(writeWait))
108 | if err != nil {
109 | return
110 | }
111 | time.Sleep(timeout / 2)
112 | if time.Now().Sub(lastResponse) > timeout {
113 | log.Info("Ping pong timeout, close client", client)
114 | cleaner.CleanClient(client)
115 | return
116 | }
117 | }
118 | }()
119 | }
120 | func (client *Client) CloseHandler() func(code int, text string) {
121 | return func(code int, text string) {
122 | message := []byte{}
123 | if code != websocket.CloseNoStatusReceived {
124 | message = websocket.FormatCloseMessage(code, "")
125 | }
126 | client.WriteControl(websocket.CloseMessage, message, time.Now().Add(writeWait))
127 | cleaner.CleanClient(client)
128 | }
129 | }
130 |
131 | type ClientBucket struct {
132 | Clients map[*websocket.Conn]*Client
133 | lck sync.RWMutex
134 | }
135 |
136 | func InitClientBucket() error {
137 | var (
138 | err error
139 | )
140 | clientBucket = &ClientBucket{
141 | Clients: make(map[*websocket.Conn]*Client),
142 | lck: sync.RWMutex{},
143 | }
144 |
145 | keepalive, err = strconv.Atoi(Conf.GetConfig("sys", "keepalive_timeout"))
146 | if err != nil {
147 | return err
148 | }
149 | return OK
150 | }
151 |
152 | func (cb *ClientBucket) Get(conn *websocket.Conn) (*Client, error) {
153 | cb.lck.RLock()
154 | defer cb.lck.RUnlock()
155 | if v, ok := cb.Clients[conn]; ok {
156 | return v, OK
157 | } else {
158 | return nil, ErrRoomDoesNotExist
159 | }
160 |
161 | }
162 |
163 | func (cb *ClientBucket) Add(client *Client) error {
164 | cb.lck.Lock()
165 | cb.Clients[client.Conn] = client
166 | cb.lck.Unlock()
167 | return OK
168 | }
169 |
170 | func (cb *ClientBucket) Remove(conn *websocket.Conn) error {
171 | if _, ok := cb.Clients[conn]; !ok {
172 | return ErrRoomDoesNotExist
173 | }
174 | cb.lck.Lock()
175 | delete(cb.Clients, conn)
176 | cb.lck.Unlock()
177 | return OK
178 | }
179 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | log "github.com/alecthomas/log4go"
8 | "github.com/larspensjo/config"
9 | "os"
10 | "runtime"
11 | )
12 |
13 | var (
14 | Conf *Config
15 | appPath = os.Getenv("GOPATH") + "/src/github.com/kong36088/danmu/"
16 | configFile = flag.String("config", appPath+"config/config.ini", "General configuration file")
17 | )
18 |
19 | type Config struct {
20 | values map[string]map[string]string
21 | }
22 |
23 | //topic list
24 |
25 | func NewConfig() *Config {
26 | return &Config{
27 | values: make(map[string]map[string]string),
28 | }
29 | }
30 |
31 | func InitConfig() error {
32 | Conf = NewConfig()
33 |
34 | runtime.GOMAXPROCS(runtime.NumCPU())
35 | flag.Parse()
36 |
37 | cfgSecs, err := config.ReadDefault(*configFile)
38 | if err != nil {
39 | return errors.New(fmt.Sprintf("Fail to find %s %s", *configFile, err))
40 | }
41 |
42 | for _, section := range cfgSecs.Sections() {
43 | options, err := cfgSecs.SectionOptions(section)
44 | if err != nil {
45 | log.Error("Read options of file %s section %s failed, %s\n", *configFile, section, err)
46 | continue
47 | }
48 | Conf.values[section] = make(map[string]string)
49 | for _, v := range options {
50 | option, err := cfgSecs.String(section, v)
51 | if err != nil {
52 | log.Error("Read file %s option %s failed, %s\n", *configFile, v, err)
53 | continue
54 | }
55 | Conf.values[section][v] = option
56 | }
57 | }
58 | return nil
59 | }
60 |
61 | func (c *Config) GetConfig(section, option string) string {
62 | return c.values[section][option]
63 | }
64 |
65 | func (c *Config) GetSectionConfig(section string) map[string]string {
66 | return c.values[section]
67 | }
68 |
69 | func (c *Config) GetAllConfig() map[string]map[string]string {
70 | return c.values
71 | }
72 |
--------------------------------------------------------------------------------
/config/config.ini:
--------------------------------------------------------------------------------
1 | [sys]
2 | port=9500
3 | keepalive_timeout=60 #心跳检测超时时间,设置为0则不进行检测
4 | log_conf=./config/log4go.xml
5 | push_freq=1 #弹幕推送时延,默认1秒推送一次
6 |
7 | [kafka]
8 | address=localhost:9092
9 | group=danmu_c
10 | topic=danmu
--------------------------------------------------------------------------------
/config/log4go.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | stdout
4 | console
5 |
6 | DEBUG
7 |
8 |
9 | debug_file
10 | file
11 | DEBUG
12 | /var/log/danmu/danmu_debug.log
13 | [%D %T] [%L] [%S] %M
14 | true
15 | 0M
16 | 0K
17 | true
18 |
19 |
20 | info_file
21 | file
22 | INFO
23 | /var/log/danmu/danmu_info.log
24 |
35 | [%D %T] [%L] [%S] %M
36 | true
37 | 0M
38 | 0K
39 | true
40 |
41 |
42 | warn_file
43 | file
44 | WARNING
45 | /var/log/danmu/danmu_warn.log
46 | [%D %T] [%L] [%S] %M
47 | true
48 | 0M
49 | 0K
50 | true
51 |
52 |
53 | error_file
54 | file
55 | ERROR
56 | /var/log/danmu/danmu_error.log
57 | [%D %T] [%L] [%S] %M
58 | true
59 | 0M
60 | 0K
61 | true
62 |
63 |
64 |
--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import "errors"
4 |
5 | var (
6 | OK error = nil
7 | //sys
8 | ErrParamError = errors.New("incorrect param")
9 |
10 | //room
11 | ErrRoomDoesNotExist = errors.New("room does not exist")
12 | )
13 |
--------------------------------------------------------------------------------
/example/client/chat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chat Example
5 |
55 |
92 |
93 |
94 |
95 |
99 |
100 |
--------------------------------------------------------------------------------
/example/client/room.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Room
7 |
8 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
108 |
109 |
110 |
111 |
269 |
270 |
--------------------------------------------------------------------------------
/example/client/room_list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Room list
6 |
7 |
18 |
19 |
20 |
21 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/example/client/static/video/iphonex.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kong36088/danmu/40ae9f888d9bfaa7a627b55739a16082934804c7/example/client/static/video/iphonex.mp4
--------------------------------------------------------------------------------
/example/serv/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/kong36088/danmu"
5 | )
6 |
7 | func main() {
8 | danmu.StartServer()
9 | }
--------------------------------------------------------------------------------
/kafka.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | "github.com/Shopify/sarama"
5 | log "github.com/alecthomas/log4go"
6 | "github.com/bsm/sarama-cluster"
7 | "strings"
8 | "time"
9 | )
10 |
11 | var (
12 | Topic string
13 | Topics []string
14 | Group string
15 | producer sarama.AsyncProducer
16 | consumer *cluster.Consumer
17 | )
18 |
19 | func InitKafka(kafkaAddrs []string) error {
20 | var (
21 | err error
22 | )
23 | Topic = Conf.GetConfig("kafka", "topic")
24 | Topics = strings.Split(Topic, ",")
25 | Group = Conf.GetConfig("kafka", "group")
26 |
27 | err = InitKafkaProducer(kafkaAddrs)
28 | if err != nil {
29 | return err
30 | }
31 | err = InitKafkaConsumer(kafkaAddrs)
32 | if err != nil {
33 | return err
34 | }
35 |
36 | return nil
37 | }
38 |
39 | func CloseKafka() {
40 | producer.Close()
41 | consumer.Close()
42 | }
43 |
44 | // producer
45 |
46 | func InitKafkaProducer(kafkaAddrs []string) error {
47 | var (
48 | err error
49 | )
50 | config := sarama.NewConfig()
51 | //config.Producer.RequiredAcks = sarama.NoResponse
52 | config.Producer.RequiredAcks = sarama.WaitForLocal
53 | config.Producer.Partitioner = sarama.NewHashPartitioner
54 | config.Producer.Return.Successes = true
55 | config.Producer.Return.Errors = true
56 | config.Producer.Timeout = 5 * time.Second
57 | producer, err = sarama.NewAsyncProducer(kafkaAddrs, config)
58 | if err != nil{
59 | return err
60 | }
61 | go handleProducerSuccess()
62 | go handleProducerError()
63 | return OK
64 | }
65 |
66 | func handleProducerSuccess() {
67 | var (
68 | pm *sarama.ProducerMessage
69 | )
70 | for {
71 | pm = <-producer.Successes()
72 | if pm != nil {
73 | log.Info("producer message success, partition:%d offset:%d key:%v valus:%s", pm.Partition, pm.Offset, pm.Key, pm.Value)
74 | }
75 | }
76 | }
77 |
78 | func handleProducerError() {
79 | var (
80 | err *sarama.ProducerError
81 | )
82 | for {
83 | err = <-producer.Errors()
84 | if err != nil {
85 | log.Error("producer message error, partition:%d offset:%d key:%v valus:%s error(%v)", err.Msg.Partition, err.Msg.Offset, err.Msg.Key, err.Msg.Value, err.Err)
86 | }
87 | }
88 | }
89 |
90 | //consumer
91 |
92 | func InitKafkaConsumer(kafkaAddrs []string) error {
93 | var (
94 | err error
95 | )
96 | config := cluster.NewConfig()
97 | config.Group.Return.Notifications = false
98 | config.Consumer.Return.Errors = true
99 | config.Consumer.Offsets.CommitInterval = 1 * time.Second
100 | config.Consumer.Offsets.Initial = sarama.OffsetNewest //初始从最新的offset开始
101 |
102 | consumer, err = cluster.NewConsumer(kafkaAddrs, Group, Topics, config)
103 | if err != nil {
104 | return err
105 | }
106 |
107 | // consume errors
108 | go handleConsumerError()
109 |
110 | return OK
111 | }
112 |
113 | func handleConsumerError() {
114 | for err := range consumer.Errors() {
115 | log.Error("Consumer Error: %s\n", err.Error())
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/log.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | log "github.com/alecthomas/log4go"
5 | )
6 |
7 | func InitLog() error {
8 | logPath := Conf.GetConfig("sys", "log_conf")
9 | log.LoadConfiguration(appPath + logPath)
10 | return OK
11 | }
12 |
13 | func CloseLog() {
14 | log.Close()
15 | }
16 |
--------------------------------------------------------------------------------
/message_handler.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | "encoding/json"
5 | log "github.com/alecthomas/log4go"
6 | "strconv"
7 | "sync"
8 | "time"
9 | )
10 |
11 | var (
12 | commandChans map[*Room]chan string
13 | lock *sync.RWMutex
14 | pushFreq int
15 | msgRoomObs RoomObserverInterface
16 | )
17 |
18 | func InitMessageHandler() error {
19 | var err error
20 |
21 | commandChans = make(map[*Room]chan string)
22 | lock = &sync.RWMutex{}
23 |
24 | pushFreq, err = strconv.Atoi(Conf.GetConfig("sys", "push_freq"))
25 | msgRoomObs = new(MessageRoomObserver)
26 |
27 | roomBucket.AttachObserver(msgRoomObs)
28 |
29 | if err != nil {
30 | return err
31 | }
32 | return OK
33 | }
34 |
35 | type MessageRoomObserver struct{}
36 |
37 | func (mro *MessageRoomObserver) Update(action int, room *Room) {
38 | lock.Lock()
39 | defer lock.Unlock()
40 |
41 | if action == RoomActionAdd {
42 | commandChans[room] = make(chan string)
43 | go messagePusher(room, commandChans[room])
44 | } else if action == RoomActionDelete {
45 | commandChans[room] <- "stop"
46 | delete(commandChans, room)
47 | }
48 |
49 | }
50 |
51 | func messageHandler() {
52 | var (
53 | proto *Proto
54 | )
55 | proto = NewProto()
56 | for {
57 | select {
58 | case msg, ok := <-consumer.Messages():
59 | if ok {
60 | //fmt.Printf("%s/%d/%d\t%s\t%s\n", msg.Topic, msg.Partition, msg.Offset, msg.Key, msg.Value)
61 | consumer.MarkOffset(msg, "") // mark message as processed
62 |
63 | if err := json.Unmarshal(msg.Value, proto); err != nil {
64 | log.Error(err)
65 | continue
66 | }
67 |
68 | roomId := proto.RoomId
69 | room, err := roomBucket.Get(rid(roomId))
70 | if err != nil {
71 | log.Error(err)
72 | continue
73 | }
74 | room.protoList.PushBack(proto)
75 | log.Debug(proto)
76 | }
77 | }
78 | }
79 | }
80 |
81 | func messagePusher(room *Room, commandChan chan string) {
82 | ticker := time.NewTicker(time.Duration(pushFreq) * time.Second) // 推送定时器
83 | for {
84 | select {
85 | case command := <-commandChan:
86 | if command == "stop" {
87 | return
88 | }
89 | case <- ticker.C:
90 | datas := room.protoList.PopAll()
91 | protoLen := len(datas)
92 | if protoLen > 0 {
93 | protos := make([]*Proto, 0, protoLen)
94 | for i := 0; i < protoLen; i++ {
95 | proto, err := datas[i].(*Proto)
96 | if err == false {
97 | log.Error("*Proto type assertion failed")
98 | continue
99 | }
100 | protos = append(protos, proto)
101 | }
102 | for _, client := range room.GetClients() {
103 | client.BatchWrite(protos)
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/proto.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | "encoding/json"
5 | log "github.com/alecthomas/log4go"
6 | )
7 |
8 | const (
9 | OPErr = "error"
10 | OPMsg = "message"
11 | )
12 |
13 | type Proto struct {
14 | OP string `json:"op"`
15 | Message string `json:"message"`
16 | RoomId rid `json:"room_id"`
17 | }
18 |
19 | func NewProto() *Proto{
20 | return &Proto{}
21 | }
22 |
23 | func (p Proto) String() string {
24 | return p.JsonEncode()
25 | }
26 |
27 | func (p *Proto) JsonEncode() string {
28 | j, err := json.Marshal(p)
29 | if err != nil {
30 | log.Error(err)
31 | return ""
32 | }
33 | return string(j)
34 | }
35 |
--------------------------------------------------------------------------------
/room.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | "fmt"
5 | log "github.com/alecthomas/log4go"
6 | "github.com/gorilla/websocket"
7 | "github.com/kong36088/danmu/utils"
8 | "strconv"
9 | "sync"
10 | )
11 |
12 | const (
13 | roomMapCup = 100
14 | )
15 |
16 | type (
17 | rid int64
18 | )
19 |
20 | //TODO room auto create
21 | func (r rid) String() string {
22 | return strconv.Itoa(int(r))
23 | }
24 |
25 | var (
26 | roomList = []rid{1, 2, 3, 4, 5, 6, 7, 8}
27 | roomBucket *RoomBucket
28 | )
29 |
30 | type Room struct {
31 | RoomId rid
32 | clients map[*websocket.Conn]*Client
33 | lck sync.RWMutex
34 | protoList *utils.ConcurrentList //Batch push
35 | }
36 |
37 | func NewRoom(roomId rid) *Room {
38 | r := new(Room)
39 | r.RoomId = roomId
40 | r.clients = make(map[*websocket.Conn]*Client)
41 | r.protoList = utils.NewConcurrentList()
42 | return r
43 | }
44 |
45 | func (room *Room) AddClient(client *Client) error {
46 | room.lck.Lock()
47 | room.clients[client.Conn] = client
48 | room.lck.Unlock()
49 |
50 | return nil
51 | }
52 |
53 | func (room *Room) GetClients() map[*websocket.Conn]*Client {
54 | room.lck.RLock()
55 | m := room.clients
56 | room.lck.RUnlock()
57 |
58 | return m
59 | }
60 |
61 | func (room Room) String() string {
62 | return fmt.Sprintf("RoomId:%s", room.RoomId)
63 | }
64 |
65 | type RoomBucket struct {
66 | Rooms map[rid]*Room
67 | lck sync.RWMutex
68 | observers map[interface{}]interface{}
69 | }
70 |
71 | func InitRoomBucket() error {
72 | roomBucket = &RoomBucket{
73 | Rooms: make(map[rid]*Room, roomMapCup),
74 | lck: sync.RWMutex{},
75 | observers: make(map[interface{}]interface{}),
76 | }
77 | for _, v := range roomList {
78 | r := NewRoom(v)
79 | err := roomBucket.Add(r)
80 | if err != nil {
81 | log.Exit(err)
82 | }
83 | }
84 | return OK
85 | }
86 |
87 | func (rb *RoomBucket) Get(id rid) (*Room, error) {
88 | rb.lck.RLock()
89 | defer rb.lck.RUnlock()
90 | if v, ok := rb.Rooms[id]; ok {
91 | return v, nil
92 | } else {
93 | return nil, ErrRoomDoesNotExist
94 | }
95 |
96 | }
97 |
98 | func (rb *RoomBucket) Add(room *Room) error {
99 | rb.lck.Lock()
100 | rb.Rooms[room.RoomId] = room
101 | rb.lck.Unlock()
102 |
103 | rb.notify(RoomActionAdd, room)
104 |
105 | return OK
106 | }
107 |
108 | func (rb *RoomBucket) Remove(room *Room) error {
109 | if _, ok := rb.Rooms[room.RoomId]; !ok {
110 | return ErrRoomDoesNotExist
111 | }
112 | rb.lck.Lock()
113 | delete(rb.Rooms, room.RoomId)
114 | rb.lck.Unlock()
115 |
116 | rb.notify(RoomActionDelete, room)
117 |
118 | return OK
119 | }
120 |
121 | func (rb *RoomBucket) AttachObserver(observer RoomObserverInterface) {
122 | rb.lck.Lock()
123 | rb.observers[observer] = observer
124 |
125 | for _, room := range rb.Rooms {
126 | observer.Update(RoomActionAdd, room)
127 | }
128 |
129 | rb.lck.Unlock()
130 | }
131 |
132 | func (rb *RoomBucket) DetachObserver(observer RoomObserverInterface) {
133 | rb.lck.Lock()
134 | delete(rb.observers, observer)
135 |
136 | for _, room := range rb.Rooms {
137 | observer.Update(RoomActionDelete, room)
138 | }
139 |
140 | rb.lck.Unlock()
141 | }
142 |
143 | func (rb *RoomBucket) notify(action int, room *Room) {
144 | rb.lck.RLock()
145 | defer rb.lck.RUnlock()
146 |
147 | for _, ob := range rb.observers {
148 | ob.(RoomObserverInterface).Update(action, room)
149 | }
150 | }
151 |
152 | const (
153 | RoomActionAdd = 1
154 | RoomActionDelete = 2
155 | )
156 |
157 | type RoomObserverInterface interface {
158 | Update(int, *Room)
159 | }
160 |
--------------------------------------------------------------------------------
/static_server.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 |
4 | import (
5 | "bytes"
6 | "errors"
7 | "fmt"
8 | "html/template"
9 | "net/http"
10 | )
11 |
12 |
13 | func StaticHandler(res http.ResponseWriter, req *http.Request) {
14 | t, err := template.ParseFiles("I:\\ubuntu14.04\\share\\docker\\go\\www\\src\\github.com\\kong36088\\danmu\\client\\index.html")
15 | if err != nil {
16 | fmt.Println(err)
17 | return
18 | }
19 | err = WriteTemplateToHttpResponse(res, t)
20 | if err != nil {
21 | fmt.Println(err)
22 | return
23 | }
24 | }
25 |
26 | func WriteTemplateToHttpResponse(res http.ResponseWriter, t *template.Template) error {
27 | if t == nil || res == nil {
28 | return errors.New("WriteTemplateToHttpResponse: t must not be nil.")
29 | }
30 | var buf bytes.Buffer
31 | err := t.Execute(&buf, nil)
32 | if err != nil {
33 | return err
34 | }
35 | res.Header().Set("Content-Type", "text/html; charset=utf-8")
36 | _, err = res.Write(buf.Bytes())
37 | return err
38 | }
--------------------------------------------------------------------------------
/utils/concurrent_list.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "container/list"
5 | "sync"
6 | )
7 |
8 | type ConcurrentList struct {
9 | lock *sync.RWMutex
10 | list *list.List
11 | }
12 |
13 | func NewConcurrentList() *ConcurrentList {
14 | return &ConcurrentList{
15 | lock: &sync.RWMutex{},
16 | list: list.New(),
17 | }
18 | }
19 |
20 | func (cl *ConcurrentList) PushBack(v interface{}) *list.Element {
21 | cl.lock.Lock()
22 |
23 | ele := cl.list.PushBack(v)
24 |
25 | cl.lock.Unlock()
26 |
27 | return ele
28 | }
29 |
30 | func (cl *ConcurrentList) Pop() interface{} {
31 | cl.lock.Lock()
32 |
33 | value := cl.list.Remove(cl.list.Back())
34 |
35 | cl.lock.Unlock()
36 |
37 | return value
38 | }
39 |
40 | func (cl *ConcurrentList) PopAll() []interface{} {
41 | datas := make([]interface{}, 0, cl.Len())
42 |
43 | if cap(datas) > 0 {
44 | cl.lock.Lock()
45 |
46 | for i := 0; i < cap(datas); i++ {
47 | datas = append(datas, cl.list.Remove(cl.list.Front()))
48 | }
49 | cl.lock.Unlock()
50 |
51 | return datas
52 | } else {
53 | return nil
54 | }
55 |
56 | }
57 |
58 | func (cl *ConcurrentList) Back() *list.Element {
59 | cl.lock.RLock()
60 |
61 | ele := cl.list.Back()
62 |
63 | cl.lock.RUnlock()
64 |
65 | return ele
66 | }
67 |
68 | func (cl *ConcurrentList) Front() *list.Element {
69 | cl.lock.RLock()
70 |
71 | ele := cl.list.Front()
72 |
73 | cl.lock.RUnlock()
74 |
75 | return ele
76 | }
77 |
78 | func (cl *ConcurrentList) Len() int {
79 | cl.lock.RLock()
80 |
81 | length := cl.list.Len()
82 |
83 | cl.lock.RUnlock()
84 |
85 | return length
86 | }
87 |
--------------------------------------------------------------------------------
/ws_server.go:
--------------------------------------------------------------------------------
1 | package danmu
2 |
3 | import (
4 | "github.com/Shopify/sarama"
5 | log "github.com/alecthomas/log4go"
6 | "github.com/gorilla/websocket"
7 | "net/http"
8 | "strconv"
9 | "strings"
10 | "time"
11 | )
12 |
13 | const (
14 | writeWait = time.Second
15 | roomIdFiled = "room"
16 | )
17 |
18 | //TODO 在线人数
19 | var
20 | (
21 | upgrader = websocket.Upgrader{
22 | ReadBufferSize: 1024,
23 | WriteBufferSize: 1024,
24 | CheckOrigin: func(r *http.Request) bool {
25 | return true
26 | }, // 解决域不一致的问题
27 | } // 将http升级为websocket
28 | )
29 |
30 | func onConnect(w http.ResponseWriter, r *http.Request) {
31 | conn, err := upgrader.Upgrade(w, r, nil)
32 | if err != nil {
33 | log.Error(err)
34 | return
35 | }
36 | client := NewClient(0, conn)
37 | roomId := r.FormValue(roomIdFiled)
38 | roomIdi, err := strconv.Atoi(roomId)
39 | if err != nil {
40 | client.WriteErrorMsg("incorrect roomId.")
41 | cleaner.CleanClient(client)
42 | log.Error("Parse roomid failed, roomid: %s, err: %s\n", roomId, err)
43 | return
44 | }
45 | room, err := roomBucket.Get(rid(roomIdi))
46 | if err != nil {
47 | client.WriteErrorMsg("Room does not exist.")
48 | cleaner.CleanClient(client)
49 | log.Error("Room does not exist, roomid: %s, err: %s\n", roomId, err)
50 | return
51 | }
52 | client.RoomId = rid(roomIdi)
53 | room.AddClient(client)
54 |
55 | clientBucket.Add(client)
56 |
57 | //send
58 | go listen(client)
59 | go client.keepAlive()
60 |
61 | }
62 |
63 | // listen message that receive from client
64 | // should be called by goroutines
65 | func listen(client *Client) {
66 | defer cleaner.CleanClient(client)
67 | for {
68 | proto := Proto{}
69 | err := client.ReadJSON(&proto)
70 | if err != nil {
71 | if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
72 | return
73 | } else {
74 | log.Error(err)
75 | return
76 | }
77 | }
78 | log.Debug(proto)
79 | msg := &sarama.ProducerMessage{
80 | Topic: Topic,
81 | Value: sarama.ByteEncoder(proto.JsonEncode()),
82 | }
83 | producer.Input() <- msg
84 | }
85 | }
86 |
87 | func StartServer() {
88 | var (
89 | err error
90 | )
91 | if err = InitConfig(); err != nil {
92 | panic(err)
93 | }
94 |
95 | if err = InitLog(); err != nil {
96 | panic(err)
97 | }
98 | defer CloseLog()
99 | log.Info("danmu server start")
100 |
101 | kafkaAddrs := Conf.GetConfig("kafka", "address")
102 | kafkaAddr := strings.Split(kafkaAddrs, ",")
103 |
104 | if err = InitKafka(kafkaAddr); err != nil {
105 | log.Exit(err)
106 | }
107 | defer CloseKafka()
108 |
109 | if err = InitRoomBucket(); err != nil {
110 | log.Exit(err)
111 | }
112 |
113 | if err = InitClientBucket(); err != nil {
114 | log.Exit(err)
115 | }
116 |
117 | if err = InitCleaner(); err != nil {
118 | log.Exit(err)
119 | }
120 |
121 | // http.HandleFunc("/", StaticHandler)
122 | http.HandleFunc("/ws", onConnect)
123 |
124 | if err = InitMessageHandler(); err != nil {
125 | log.Exit(err)
126 | }
127 | go messageHandler()
128 |
129 | addr := ":" + Conf.GetConfig("sys", "port")
130 | err = http.ListenAndServe(addr, nil)
131 | if err != nil {
132 | log.Exit(err)
133 | }
134 | }
135 |
--------------------------------------------------------------------------------